From 2a2fdb171e641933e54d6cd86ff1e2abb403e323 Mon Sep 17 00:00:00 2001 From: Pavel Eliseev Date: Sun, 4 Oct 2026 16:09:38 +0300 Subject: [PATCH 1/2] feat(ui): extend minimal doctor design across all screens Refs #85 --- .streamlit/config.toml | 12 +- dashboard/components/patient_card.py | 19 +- dashboard/console/playback_ui.py | 2 +- dashboard/console/widgets.py | 5 +- dashboard/pages/1b_Подключить_пациента.py | 6 +- dashboard/pages/2_Демо-консоль.py | 24 +- dashboard/pages/3_Сплит-экран.py | 20 +- dashboard/theme.py | 29 +- web/public/favicon.svg | 2 +- web/src/components/AppShell.tsx | 29 +- web/src/components/Icon.tsx | 4 +- web/src/content/ru.ts | 3 + web/src/main.tsx | 4 - web/src/theme/global.css | 493 +++++++++++++++++----- web/src/theme/tokens.css | 66 +-- 15 files changed, 526 insertions(+), 192 deletions(-) diff --git a/.streamlit/config.toml b/.streamlit/config.toml index c66a55f..4900afa 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -1,15 +1,15 @@ # Dashboard-only Streamlit settings (doctor dashboard + demo console). [theme] base = "light" -primaryColor = "#2A78D6" -backgroundColor = "#F6F7F9" +primaryColor = "#285E4E" +backgroundColor = "#F6F7F3" secondaryBackgroundColor = "#FFFFFF" -textColor = "#1B1F24" -borderColor = "#E3E6EA" +textColor = "#213B38" +borderColor = "#E1E7E3" baseRadius = "medium" # Falls back to the system sans-serif when offline (ADR-004). -font = "Inter:https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap, sans-serif" -headingFontWeights = 700 +font = "sans-serif" +headingFontWeights = 600 [client] toolbarMode = "minimal" diff --git a/dashboard/components/patient_card.py b/dashboard/components/patient_card.py index 91b7764..7b08d72 100644 --- a/dashboard/components/patient_card.py +++ b/dashboard/components/patient_card.py @@ -42,7 +42,10 @@ def dose_strip_html(grid: h.DoseGrid, start: date | None) -> str: f'ожидается · {c["pending"]}' "" ) - return f'
{"".join(cells)}
{legend}' + return ( + f'
' + f"{''.join(cells)}
{legend}" + ) def _tile(label: str, value: str, hint: str = "", tone: str = "blue") -> str: @@ -181,7 +184,7 @@ def render(card: PatientCard, today: date | None) -> None: main, side = st.columns([1.75, 1], gap="large") with main: - with st.container(border=True): + with st.container(border=True, key="panel-scales"): c1, c2 = st.columns(2) for col, scale_id, title in ((c1, "phq9", "PHQ-9"), (c2, "pcl5", "PCL-5")): fig = charts.scale_figure(card.series, scale_id, title, start) @@ -191,22 +194,22 @@ def render(card: PatientCard, today: date | None) -> None: else: st.plotly_chart(fig, config=theme.PLOTLY_CONFIG, key=f"chart-{scale_id}") st.caption("◇ — внеплановое заполнение (по триггеру правил). Полосы — градации тяжести из config/scales.") - with st.container(border=True): + with st.container(border=True, key="panel-wellbeing"): fig = charts.wellbeing_figure(card.series, start) if fig is None: st.caption("Ежедневных чек-инов пока нет") else: st.plotly_chart(fig, config=theme.PLOTLY_CONFIG, key="chart-wellbeing") st.caption("Красная зона настроения ≤ 3 (3 дня → внеплановый PHQ-9); пунктир — сон 4 ч.") - with st.container(border=True): + with st.container(border=True, key="panel-doses"): st.html('
Календарь приёма
') st.html(dose_strip_html(grid, start)) with side: - with st.container(border=True): + with st.container(border=True, key="panel-alerts"): _alerts_feed(card.alerts, today) - with st.container(border=True): + with st.container(border=True, key="panel-summary"): _summary(card.summary) - with st.container(border=True): + with st.container(border=True, key="panel-safety"): _safety_plan(card.safety_plan) - with st.container(border=True): + with st.container(border=True, key="panel-prescriptions"): _prescriptions(card) diff --git a/dashboard/console/playback_ui.py b/dashboard/console/playback_ui.py index a678125..21bdd04 100644 --- a/dashboard/console/playback_ui.py +++ b/dashboard/console/playback_ui.py @@ -51,7 +51,7 @@ def render_playback_panel(persona: dict[str, Any]) -> None: token = persona["token"] scenario, source = w.scenario_for(persona) state = w.playback_state(token) - st.subheader("🎬 Сценарий демо") + st.subheader("Сценарий демо") _scenario_source_controls(persona, source) if scenario.errors: with st.expander(f"⚠️ Замечания к сценарию: {len(scenario.errors)}", expanded=not scenario.days): diff --git a/dashboard/console/widgets.py b/dashboard/console/widgets.py index 3b19d75..d1a8e3e 100644 --- a/dashboard/console/widgets.py +++ b/dashboard/console/widgets.py @@ -368,10 +368,9 @@ def render_doctor_panel(highlight_token: str | None, *, key: str = "doc") -> Non ] for a in fresh_acute[:1]: st.markdown( - "
" + "
" f"🚨 ОСТРЫЙ АЛЕРТ · {_escape(a.get('patient_name', ''))} · {_fmt_ts(a.get('ts'))}
" - f"{_escape(a.get('reason', ''))}
", + f"
{_escape(a.get('reason', ''))}
", unsafe_allow_html=True, ) c1, c2 = st.columns(2) diff --git a/dashboard/pages/1b_Подключить_пациента.py b/dashboard/pages/1b_Подключить_пациента.py index 18386b1..4b9a37a 100644 --- a/dashboard/pages/1b_Подключить_пациента.py +++ b/dashboard/pages/1b_Подключить_пациента.py @@ -68,7 +68,7 @@ def _reset() -> None: def _result(res: ConnectPatientOut, name: str) -> None: link = h.absolute_link(res.deep_link, os.environ.get("DASHBOARD_PUBLIC_URL", "http://localhost:8501")) - with st.container(border=True): + with st.container(border=True, key="panel-connect-result"): st.html( f'
Пациент подключён · {escape(name)}
' '
Покажите QR пациенту или продиктуйте код — бот спросит ' @@ -108,7 +108,7 @@ def main() -> None: st.button("Заполнить демо-данными (Андрей)", icon=":material/auto_fix_high:", on_click=_fill_demo, args=(today,)) left, right = st.columns(2, gap="large") - with left, st.container(border=True): + with left, st.container(border=True, key="panel-connect-person"): st.html('
Пациент
') st.text_input("Имя и инициал фамилии", key="cp_name", placeholder="Андрей С.", help="Без полного ФИО") a, b = st.columns(2) @@ -128,7 +128,7 @@ def main() -> None: key="cp_modules", format_func=lambda m: h.MODULE_LABELS[m], ) - with right, st.container(border=True): + with right, st.container(border=True, key="panel-connect-prescription"): st.html('
Назначение
') d1, d2 = st.columns([1.4, 1]) with d1: diff --git a/dashboard/pages/2_Демо-консоль.py b/dashboard/pages/2_Демо-консоль.py index 903cb5e..427330c 100644 --- a/dashboard/pages/2_Демо-консоль.py +++ b/dashboard/pages/2_Демо-консоль.py @@ -14,21 +14,31 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from console import widgets as w # noqa: E402 from console.playback_ui import render_playback_panel # noqa: E402 +from dashboard import theme # noqa: E402 +from dashboard.components import ui # noqa: E402 + st.set_page_config(page_title="Демо-консоль", page_icon="🎬", layout="wide") +st.html(theme.CSS) +st.html( + ui.page_header( + "Демо-консоль", + "Чат пациента и сценарий сопровождения. Прототип на синтетических данных.", + kicker="Демонстрация", + ) +) + persona = w.select_persona() w.render_connect_hero() w.show_flash() st.sidebar.caption(f"API: {w.get_api().base_url}") -st.title("Демо-консоль") -st.caption("Прототип на синтетических данных. Время — виртуальные демо-часы (ADR-005).") -w.render_clock_bar(persona, key="console-clock") -st.divider() +with st.container(border=True, key="panel-console-clock"): + w.render_clock_bar(persona, key="console-clock") chat_col, play_col = st.columns([1.05, 1], gap="large") -with chat_col: - st.subheader(f"💬 Чат пациента — {persona.get('display_name') or persona['token']}") +with chat_col, st.container(border=True, key="panel-console-chat"): + st.subheader(f"Чат пациента — {persona.get('display_name') or persona['token']}") w.render_chat(persona["token"], height=560, key="console-chat") -with play_col: +with play_col, st.container(border=True, key="panel-console-scenario"): render_playback_panel(persona) diff --git a/dashboard/pages/3_Сплит-экран.py b/dashboard/pages/3_Сплит-экран.py index 2403d3f..8d4b26f 100644 --- a/dashboard/pages/3_Сплит-экран.py +++ b/dashboard/pages/3_Сплит-экран.py @@ -15,12 +15,12 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from console import widgets as w # noqa: E402 +from dashboard import theme # noqa: E402 +from dashboard.components import ui # noqa: E402 + st.set_page_config(page_title="Сплит-экран", page_icon="🖥️", layout="wide", initial_sidebar_state="collapsed") -st.markdown( - "", - unsafe_allow_html=True, -) +st.html(theme.CSS) +st.html(ui.page_header("Сплит-экран", "Пациент и врач: сопровождение в реальном времени.", kicker="Демонстрация")) persona = w.select_persona() w.show_flash() @@ -44,15 +44,15 @@ if clicked and nxt is not None: st.rerun() left, right = st.columns([1.1, 1], gap="large") -with left: +with left, st.container(border=True, key="panel-split-patient"): url = w.patient_web_url() if url: - st.markdown(f"#### 📱 Пациент — {persona.get('display_name') or token}") + st.markdown(f"#### Пациент — {persona.get('display_name') or token}") st.iframe(url, height=760) st.caption(f"Веб-приложение пациента: {url}") else: - st.markdown(f"#### 💬 Пациент — {persona.get('display_name') or token}") + st.markdown(f"#### Пациент — {persona.get('display_name') or token}") w.render_chat(token, height=600, key="split-chat") -with right: - st.markdown("#### 🩺 Кабинет врача — д-р Смирнова Е. А.") +with right, st.container(border=True, key="panel-split-doctor"): + st.markdown("#### Кабинет врача — д-р Смирнова Е. А.") w.polled_doctor_panel(token, key="split-doc") diff --git a/dashboard/theme.py b/dashboard/theme.py index efc4aa9..76ee726 100644 --- a/dashboard/theme.py +++ b/dashboard/theme.py @@ -138,8 +138,8 @@ div[class*="st-key-prow-"] {{ border-top:1px solid var(--line); padding:18px 0 4 .mt-empty p {{ font-size:.85rem; margin:8px 0 0; }} /* ---------- chips, badges, bars ---------- */ -.mt-chip {{ display:inline-block; font-size:.72rem; padding: 1px 8px; border-radius: 999px; margin-right: 4px; - background: var(--blue-tint); color: var(--blue-ink); font-weight: 500; }} +.mt-chip {{ display:inline-block; font-size:.72rem; padding: 1px 8px; border-radius: 5px; margin-right: 4px; + background: #edf3ee; color: #38634b; font-weight: 500; }} .mt-badge {{ display:inline-flex; align-items:center; gap:6px; font-size:.7rem; font-weight:550; padding: 5px 8px; border-radius: 5px; white-space: normal; }} .mt-badge.t-red {{ background: var(--red-tint); color: var(--red-ink); }} @@ -166,7 +166,7 @@ div[class*="st-key-prow-"] {{ border-top:1px solid var(--line); padding:18px 0 4 .mt-banner b {{ color: inherit; }} .mt-pulse {{ width: 12px; height: 12px; border-radius: 50%; background: var(--red); margin-top: 5px; flex: none; box-shadow: 0 0 0 4px #faeeee; }} -.mt-panel-title {{ font-size: .8rem; text-transform: uppercase; letter-spacing: .07em; font-weight: 650; +.mt-panel-title {{ font-size: .82rem; text-transform: uppercase; letter-spacing: .03em; font-weight: 550; color: var(--ink-3); margin: 2px 0 8px; }} .mt-alert {{ border-left: 4px solid var(--grey); padding: 4px 0 4px 12px; }} .mt-alert.t-red {{ border-left-color: var(--red); }} @@ -175,9 +175,9 @@ div[class*="st-key-prow-"] {{ border-top:1px solid var(--line); padding:18px 0 4 .mt-alert .when {{ color: var(--ink-3); font-size: .78rem; }} .mt-alert .why {{ color: var(--ink); font-weight: 600; font-size: .92rem; margin: 2px 0; }} .mt-alert.acked .why {{ color: var(--ink-3); font-weight: 500; }} -.mt-summary {{ background: var(--blue-tint); border-radius: 12px; padding: 14px 16px; color: var(--ink); - line-height: 1.5; font-size: .93rem; }} -.mt-summary .disc {{ color: var(--blue-ink); font-size: .76rem; margin-top: 8px; }} +.mt-summary {{ background: #f4f6f2; border:1px solid var(--line); border-radius: 12px; padding: 14px 16px; + color: var(--ink); line-height: 1.5; font-size: .93rem; }} +.mt-summary .disc {{ color: var(--ink-2); font-size: .76rem; margin-top: 8px; }} .mt-plan ol {{ margin: 0; padding-left: 1.2rem; }} .mt-plan li {{ margin: 3px 0; color: var(--ink); font-size: .9rem; }} @@ -195,6 +195,21 @@ div[class*="st-key-prow-"] {{ border-top:1px solid var(--line); padding:18px 0 4 .mt-legend i {{ display:inline-block; width: 12px; height: 12px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }} +/* ---------- internal pages ---------- */ +div[class*="st-key-panel-"] {{ background:var(--surface); border:1px solid var(--line); border-radius:12px; + padding:20px; }} +[data-testid="stExpander"] {{ background:var(--surface); border-color:var(--line); border-radius:12px; }} +[data-testid="stChatMessage"] {{ background:#f4f6f2; border:1px solid var(--line); border-radius:12px; }} +[data-testid="stChatInput"] {{ background:var(--surface); border:1px solid var(--line); border-radius:8px; }} +[data-testid="stNumberInputContainer"], [data-testid="stDateInput"] input {{ + background:var(--surface); color:var(--ink); border-color:var(--line); border-radius:8px; }} +[data-testid="stApp"] [data-variant="pills"][data-selected] {{ background:#edf3ee; color:#285e4e; + border-color:#b4c6ba; }} +.mt-console-alert {{ font-size:1.1rem; font-weight:600; }} +.mt-console-alert .mt-sub {{ color:var(--red-ink); font-weight:400; }} +.mt-dose-scroll {{ overflow-x:auto; padding-bottom:4px; }} +.mt-doses {{ min-width:max-content; }} + /* ---------- connect result ---------- */ .mt-code {{ font-size: 2.4rem; font-weight: 700; letter-spacing: .12em; color: var(--ink); font-variant-numeric: tabular-nums; }} @@ -222,6 +237,8 @@ div[class*="st-key-prow-"] {{ border-top:1px solid var(--line); padding:18px 0 4 .mt-mobile-label {{ display:block; color:var(--ink-3); font-size:.68rem; margin-bottom:5px; }} .mt-row-details {{ grid-template-columns:1fr; gap:12px; }} .mt-list-footer {{ flex-wrap:wrap; }} + .mt-head .mt-side {{ text-align:left; }} + div[class*="st-key-panel-"] {{ padding:16px; }} .mt-tip:hover::after, .mt-tip:focus-visible::after {{ display:none; }} }} @media (prefers-reduced-motion: reduce) {{ diff --git a/web/public/favicon.svg b/web/public/favicon.svg index 7830f3e..0001347 100644 --- a/web/public/favicon.svg +++ b/web/public/favicon.svg @@ -1 +1 @@ - + diff --git a/web/src/components/AppShell.tsx b/web/src/components/AppShell.tsx index 1fde105..a93786e 100644 --- a/web/src/components/AppShell.tsx +++ b/web/src/components/AppShell.tsx @@ -3,13 +3,14 @@ import { Link, NavLink } from "react-router-dom"; import { ru } from "../content/ru"; import { useDialog } from "../dialog/DialogContext"; +import { useSession } from "../session/SessionContext"; import { HotlineCard, useHelp } from "./HelpSheet"; import { Icon, LogoMark, type IconName } from "./Icon"; /* - * Layout from docs/mock (series 2). + * Shared patient workspace, following the approved doctor dashboard. * phone: content → «Экстренная помощь» block → bottom tab bar (signed in) - * desktop: navy sidebar | content | right rail (optional `aside` + emergency panel) + * desktop: white sidebar | workspace header | content + emergency panel * bare: one centred column on every width (questionnaires) */ @@ -87,6 +88,10 @@ function Sidebar({ signedIn }: { signedIn: boolean }) { )} +
+ {ru.product.supportTitle} + {ru.product.supportText} +
); } @@ -122,6 +127,24 @@ function Rail({ aside }: { aside?: ReactNode }) { ); } +function WorkspaceHeader({ signedIn, bare = false }: { signedIn: boolean; bare?: boolean }) { + const { session } = useSession(); + return ( +
+
+ + {bare ? ru.product.name : signedIn ? ru.product.workspace : ru.nav.signIn} +
+ {signedIn && !bare ? ( + + + {session?.displayName || ru.nav.profile} + + ) : {ru.product.tagline}} +
+ ); +} + export function AppShell({ children, signedIn = true, @@ -144,6 +167,7 @@ export function AppShell({ return (
+
{children} {sos && } @@ -155,6 +179,7 @@ export function AppShell({
+
{children} {sos && } diff --git a/web/src/components/Icon.tsx b/web/src/components/Icon.tsx index cf90800..05510a8 100644 --- a/web/src/components/Icon.tsx +++ b/web/src/components/Icon.tsx @@ -163,8 +163,8 @@ export function Icon({ name, size = 22, label }: { name: IconName; size?: number export function LogoMark({ size = 32 }: { size?: number }) { return ( ); } diff --git a/web/src/content/ru.ts b/web/src/content/ru.ts index 44e0c74..37e1b28 100644 --- a/web/src/content/ru.ts +++ b/web/src/content/ru.ts @@ -6,6 +6,9 @@ export const ru = { product: { name: "Сопровождение пациента", tagline: "между визитами к врачу", + workspace: "Личный кабинет", + supportTitle: "Рядом между визитами", + supportText: "Наблюдение за самочувствием и поддержка назначенного лечения.", }, nav: { today: "Сегодня", diff --git a/web/src/main.tsx b/web/src/main.tsx index 0b98700..4b51591 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,7 +1,3 @@ -import "@fontsource/golos-text/400.css"; -import "@fontsource/golos-text/500.css"; -import "@fontsource/golos-text/600.css"; -import "@fontsource/golos-text/700.css"; import "./theme/tokens.css"; import "./theme/global.css"; diff --git a/web/src/theme/global.css b/web/src/theme/global.css index 5d351c2..672710b 100644 --- a/web/src/theme/global.css +++ b/web/src/theme/global.css @@ -1,4 +1,4 @@ -/* Phone-first (docs/mock series 2). ≥1024px: navy sidebar | content | emergency rail. Tokens only. */ +/* Phone-first patient workspace, matching the doctor dashboard. */ *, *::before, @@ -122,15 +122,15 @@ button:disabled { } .tabbar__item.active { - color: var(--c-navy); - font-weight: 700; + color: var(--c-primary); + font-weight: 600; } /* ---------------------------------------------------------------- type */ .h1 { font-size: var(--fs-28); - font-weight: 700; + font-weight: 550; line-height: 1.15; letter-spacing: -0.02em; color: var(--c-text); @@ -139,13 +139,13 @@ button:disabled { .h2 { font-size: var(--fs-20); - font-weight: 700; + font-weight: 550; line-height: 1.2; } .h3 { font-size: var(--fs-18); - font-weight: 700; + font-weight: 600; } .lead { @@ -196,7 +196,7 @@ button:disabled { } .tel { - font-weight: 700; + font-weight: 600; white-space: nowrap; color: var(--c-red-strong); } @@ -239,7 +239,7 @@ button:disabled { .topbar__title { font-size: var(--fs-20); - font-weight: 700; + font-weight: 600; } .divider { @@ -284,30 +284,30 @@ button:disabled { } .btn--primary { - background: var(--c-navy); + background: var(--c-primary); color: #fff; } .btn--primary:hover:not(:disabled) { - background: var(--c-navy-hover); + background: var(--c-primary-hover); } .btn--outline { background: var(--c-surface); - border-color: var(--c-navy); - color: var(--c-navy); + border-color: var(--c-primary); + color: var(--c-primary); } .btn--ghost { background: var(--c-tile); border-color: var(--c-border); - color: var(--c-navy); + color: var(--c-primary); } .btn--soft { background: var(--c-blue-soft); - color: var(--c-navy); - border-color: #cfe0f5; + color: var(--c-primary); + border-color: var(--c-border); } .btn--danger { @@ -317,7 +317,7 @@ button:disabled { .btn--primary:disabled, .btn--danger:disabled { - background: #c5cfdc; + background: var(--c-border-strong); color: #fff; } @@ -330,7 +330,7 @@ button:disabled { place-items: center; margin-left: -10px; border: 0; - border-radius: 12px; + border-radius: var(--r-tile); background: transparent; color: var(--c-text); } @@ -366,7 +366,7 @@ button:disabled { } .chip--teal { - background: #cdeee8; + background: var(--c-teal-soft); color: var(--c-teal-strong); } @@ -381,9 +381,9 @@ button:disabled { height: 48px; display: grid; place-items: center; - border-radius: 14px; + border-radius: var(--r-btn); background: var(--c-tile); - color: var(--c-navy); + color: var(--c-primary); } .tile-icon--teal { @@ -435,7 +435,7 @@ button:disabled { border-radius: 50%; background: var(--c-blue-soft); color: var(--c-blue); - font-weight: 700; + font-weight: 600; } .avatar--lg { @@ -514,6 +514,7 @@ button:disabled { /* ---------------------------------------------------------------- cards */ .panel { + border: 1px solid var(--c-border); display: grid; gap: var(--s-3); padding: var(--s-4); @@ -541,7 +542,7 @@ button:disabled { display: flex; align-items: center; gap: var(--s-2); - font-weight: 700; + font-weight: 600; } .iconlist { @@ -665,7 +666,7 @@ button.row-card:hover:not(:disabled) { .signin__name { display: block; font-size: var(--fs-18); - font-weight: 700; + font-weight: 600; color: var(--c-text); } @@ -679,9 +680,9 @@ button.row-card:hover:not(:disabled) { display: grid; gap: var(--s-5); padding: var(--s-8) var(--s-6); - border-radius: 24px; + border-radius: var(--r-tile); background: var(--c-surface); - box-shadow: 0 12px 40px rgba(15, 39, 71, 0.08); + border: 1px solid var(--c-border); } .signin__card .entry__title { @@ -708,6 +709,8 @@ button.row-card:hover:not(:disabled) { } .signin__foot { + border: 1px solid var(--c-red-line); + border-left: 3px solid var(--c-red); display: flex; align-items: center; justify-content: space-between; @@ -718,16 +721,21 @@ button.row-card:hover:not(:disabled) { } .signin__sos { + border: 0; + background: transparent; + padding: 8px 0; + min-height: 44px; + text-align: left; display: inline-flex; align-items: center; gap: var(--s-2); - font-weight: 700; + font-weight: 600; color: var(--c-red-strong); } .signin__tel { font-size: var(--fs-20); - font-weight: 700; + font-weight: 600; color: var(--c-red-strong); text-decoration: none; } @@ -742,7 +750,7 @@ button.row-card:hover:not(:disabled) { gap: var(--s-3); padding: var(--s-5); border: 1.5px solid var(--c-border); - border-radius: 20px; + border-radius: var(--r-tile); background: var(--c-surface); text-align: left; color: var(--c-text); @@ -777,7 +785,7 @@ button.row-card:hover:not(:disabled) { align-items: center; justify-content: space-between; font-size: var(--fs-20); - font-weight: 700; + font-weight: 600; } .entry__title svg { @@ -829,7 +837,7 @@ button.row-card:hover:not(:disabled) { min-height: 60px; padding: var(--s-3) var(--s-4); border: 1.5px solid var(--c-border); - border-radius: 14px; + border-radius: var(--r-btn); background: var(--c-surface); text-align: left; font-size: var(--fs-16); @@ -856,7 +864,7 @@ button.row-card:hover:not(:disabled) { .option.is-on { background: var(--c-teal-soft); - border-color: var(--c-teal-soft); + border-color: var(--c-teal); } .option.is-on .option__mark { @@ -873,9 +881,9 @@ button.row-card:hover:not(:disabled) { .numscale__value { justify-self: center; font-size: var(--fs-48); - font-weight: 700; + font-weight: 600; line-height: 1; - color: var(--c-navy); + color: var(--c-primary); } .numscale__row { @@ -947,7 +955,7 @@ button.row-card:hover:not(:disabled) { gap: var(--s-3); justify-items: center; padding: var(--s-6) var(--s-4); - border-radius: 20px; + border-radius: var(--r-tile); background: var(--c-teal-soft); } @@ -970,8 +978,8 @@ button.row-card:hover:not(:disabled) { border-radius: var(--r-tile); background: var(--c-surface); font-size: var(--fs-28); - font-weight: 700; - color: var(--c-navy); + font-weight: 600; + color: var(--c-primary); } .reminder__time svg { @@ -994,9 +1002,9 @@ button.row-card:hover:not(:disabled) { min-height: 60px; padding: 0 var(--s-4); border: 0; - border-radius: 14px; + border-radius: var(--r-btn); background: var(--c-blue-soft); - color: var(--c-navy); + color: var(--c-primary); font-size: var(--fs-18); font-weight: 600; } @@ -1057,7 +1065,7 @@ button.row-card:hover:not(:disabled) { } .breath__title { - font-weight: 700; + font-weight: 600; } .breath__viz { @@ -1074,7 +1082,7 @@ button.row-card:hover:not(:disabled) { border-radius: 50%; border: 6px solid var(--c-teal-soft); border-top-color: var(--c-teal); - border-right-color: #7fd1c6; + border-right-color: var(--c-border-strong); animation: breathe 10s var(--ease) infinite; } @@ -1206,7 +1214,7 @@ button.row-card:hover:not(:disabled) { } .task__title { - font-weight: 700; + font-weight: 600; } .task__sub { @@ -1214,13 +1222,14 @@ button.row-card:hover:not(:disabled) { color: var(--c-text-2); overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; + white-space: normal; } .task--hero { - background: var(--c-navy); - border-color: var(--c-navy); - color: #fff; + background: var(--c-teal-soft); + border-color: var(--c-border-strong); + border-left: 3px solid var(--c-primary); + color: var(--c-text); } .task--hero .task__icon { @@ -1230,7 +1239,7 @@ button.row-card:hover:not(:disabled) { .task--hero .task__sub, .task--hero > svg:last-child { - color: #c9d6e8; + color: var(--c-text-2); } .done-row { @@ -1257,7 +1266,7 @@ button.row-card:hover:not(:disabled) { display: grid; gap: var(--s-3); padding: var(--s-6) var(--s-5); - border-radius: 20px; + border-radius: var(--r-tile); background: var(--c-teal-soft); } @@ -1277,13 +1286,13 @@ button.row-card:hover:not(:disabled) { border-radius: var(--r-tile); background: var(--c-surface); text-align: left; - color: var(--c-navy); + color: var(--c-primary); } .quick__title { display: block; font-size: var(--fs-14); - font-weight: 700; + font-weight: 600; } .quick__sub { @@ -1368,7 +1377,7 @@ button.row-card:hover:not(:disabled) { .rx__drug { display: block; font-size: var(--fs-18); - font-weight: 700; + font-weight: 600; } .rx__by { @@ -1457,7 +1466,7 @@ button.row-card:hover:not(:disabled) { .bubble p { padding: var(--s-3) var(--s-4); - border-radius: 18px; + border-radius: var(--r-tile); white-space: pre-wrap; overflow-wrap: anywhere; } @@ -1504,7 +1513,7 @@ button.row-card:hover:not(:disabled) { border-radius: var(--r-btn); background: var(--c-surface); font-weight: 600; - color: var(--c-navy); + color: var(--c-primary); } .qr:first-child { @@ -1560,10 +1569,12 @@ button.row-card:hover:not(:disabled) { /* ---------------------------------------------------------------- emergency */ .sosblock { + border: 1px solid var(--c-red-line); + border-left: 3px solid var(--c-red); display: grid; gap: var(--s-2); padding: var(--s-2); - border-radius: 18px; + border-radius: var(--r-tile); background: var(--c-red-soft); } @@ -1576,7 +1587,7 @@ button.row-card:hover:not(:disabled) { border: 0; background: none; color: var(--c-red-strong); - font-weight: 700; + font-weight: 600; font-size: var(--fs-16); text-align: left; } @@ -1629,7 +1640,7 @@ button.row-card:hover:not(:disabled) { .hotline--sm { padding: var(--s-2) var(--s-3); background: rgb(255 255 255 / 0.65); - border-radius: 12px; + border-radius: var(--r-tile); } .hotline__icon { @@ -1658,7 +1669,7 @@ button.row-card:hover:not(:disabled) { .hotline__phone { font-size: var(--fs-28); - font-weight: 700; + font-weight: 600; line-height: 1.1; color: var(--c-red-strong); } @@ -1704,14 +1715,14 @@ button.row-card:hover:not(:disabled) { margin: auto auto 0; padding: 0; border: 0; - border-radius: 24px 24px 0 0; + border-radius: 16px 16px 0 0; background: var(--c-surface); color: var(--c-text); } .sheet::backdrop, .modal::backdrop { - background: rgb(15 39 71 / 0.5); + background: rgb(33 59 56 / 0.35); } .sheet[open], @@ -1739,7 +1750,7 @@ button.row-card:hover:not(:disabled) { .sheet__title { font-size: var(--fs-20); - font-weight: 700; + font-weight: 600; } .sos { @@ -1755,7 +1766,7 @@ button.row-card:hover:not(:disabled) { display: grid; place-items: center; border-radius: 50%; - background: radial-gradient(circle, var(--c-red-soft) 55%, #fff6f6 56% 100%); + background: var(--c-red-soft); } .sos__btn { @@ -1790,12 +1801,14 @@ button.row-card:hover:not(:disabled) { .app { display: grid; - grid-template-columns: var(--sidebar-w) 1fr; + grid-template-columns: var(--sidebar-w) minmax(0, 1fr); } .tabbar, .only-phone, .screen > .sosblock { + border: 1px solid var(--c-red-line); + border-left: 3px solid var(--c-red); display: none; } @@ -1811,21 +1824,22 @@ button.row-card:hover:not(:disabled) { align-content: start; gap: var(--s-8); padding: var(--s-6) var(--s-4); - background: var(--c-navy); - color: #fff; + background: var(--c-surface); + border-right: 1px solid var(--c-border); + color: var(--c-text); } .sidebar__brand { display: flex; gap: var(--s-3); align-items: center; - color: #fff; + color: var(--c-text); text-decoration: none; } .sidebar__name { display: block; - font-weight: 700; + font-weight: 600; font-size: var(--fs-14); line-height: 1.2; } @@ -1833,7 +1847,7 @@ button.row-card:hover:not(:disabled) { .sidebar__tag { display: block; font-size: var(--fs-12); - color: #a9b9cf; + color: var(--c-text-3); } .sidebar__list { @@ -1852,7 +1866,7 @@ button.row-card:hover:not(:disabled) { border: 0; border-radius: var(--r-btn); background: none; - color: #c9d6e8; + color: var(--c-text-2); font-size: var(--fs-16); font-weight: 500; text-decoration: none; @@ -1860,25 +1874,26 @@ button.row-card:hover:not(:disabled) { } .sidebar__link:hover { - background: rgb(255 255 255 / 0.06); - color: #fff; + background: var(--c-tile); + color: var(--c-primary); } .sidebar__link.active { - background: rgb(255 255 255 / 0.12); - color: #fff; - font-weight: 700; + background: var(--c-primary-soft); + color: var(--c-primary); + font-weight: 600; } .sidebar__link .dot { position: static; margin-left: auto; - border-color: var(--c-navy); + border-color: var(--c-surface); } .app__main { display: grid; - grid-template-columns: minmax(0, 1fr) var(--rail-w); + grid-template-columns: minmax(0, 1fr); + align-content: start; gap: var(--s-6); max-width: 1320px; padding: var(--s-8) var(--s-8) var(--s-8) var(--s-10); @@ -1895,18 +1910,18 @@ button.row-card:hover:not(:disabled) { /* 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; + border-radius: var(--r-tile); background: var(--c-surface); - box-shadow: 0 1px 2px rgb(15 39 71 / 0.06); + border: 1px solid var(--c-border); } /* 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; + border-radius: var(--r-tile); background: var(--c-surface); - box-shadow: 0 1px 2px rgb(15 39 71 / 0.06); + border: 1px solid var(--c-border); } .done { @@ -1920,7 +1935,7 @@ button.row-card:hover:not(:disabled) { } .split { - grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr); gap: var(--s-12); align-items: start; } @@ -1932,7 +1947,7 @@ button.row-card:hover:not(:disabled) { .split__act { padding: var(--s-6); - border-radius: 20px; + border-radius: var(--r-tile); background: var(--c-tile); } @@ -1991,7 +2006,7 @@ button.row-card:hover:not(:disabled) { /* Today: dashboard. */ .dash { - grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr); gap: var(--s-6); align-items: start; } @@ -2003,19 +2018,18 @@ button.row-card:hover:not(:disabled) { .dash__main { padding: var(--s-6); - border-radius: 24px; + border-radius: var(--r-tile); background: var(--c-surface); - box-shadow: 0 1px 2px rgb(15 39 71 / 0.06); + border: 1px solid var(--c-border); } .dash__side .card { - border-color: transparent; - box-shadow: 0 1px 2px rgb(15 39 71 / 0.06); + border: 1px solid var(--c-border); } .grid2, .cols { - grid-template-columns: 1fr 1fr; + grid-template-columns: minmax(0, 1fr); align-items: start; } @@ -2028,7 +2042,7 @@ button.row-card:hover:not(:disabled) { } .rail { - position: sticky; + position: static; top: var(--s-8); display: grid; gap: var(--s-4); @@ -2036,10 +2050,12 @@ button.row-card:hover:not(:disabled) { } .railsos { + border: 1px solid var(--c-red-line); + border-left: 3px solid var(--c-red); display: grid; gap: var(--s-2); padding: var(--s-3); - border-radius: 20px; + border-radius: var(--r-tile); background: var(--c-red-soft); } @@ -2052,7 +2068,7 @@ button.row-card:hover:not(:disabled) { border: 0; background: none; color: var(--c-red-strong); - font-weight: 700; + font-weight: 600; text-align: left; } @@ -2081,7 +2097,7 @@ button.row-card:hover:not(:disabled) { .modal { max-width: 460px; margin: auto; - border-radius: 24px; + border-radius: var(--r-tile); } .composer { @@ -2118,13 +2134,13 @@ button.row-card:hover:not(:disabled) { .chip--trauma { background: var(--c-amber-soft); - color: #a86a00; + color: var(--c-amber); } .chip--alcohol, .chip--about { - background: var(--c-navy-soft); - color: var(--c-navy); + background: var(--c-primary-soft); + color: var(--c-primary); } .survey { @@ -2243,7 +2259,7 @@ button.row-card:hover:not(:disabled) { .drinks tbody th { white-space: nowrap; - color: var(--c-navy); + color: var(--c-primary); } /* diary: hours of sleep */ @@ -2280,8 +2296,8 @@ button.row-card:hover:not(:disabled) { outline: none; background: none; font-size: var(--fs-28); - font-weight: 700; - color: var(--c-navy); + font-weight: 600; + color: var(--c-primary); } .hours__field span { @@ -2309,7 +2325,7 @@ button.row-card:hover:not(:disabled) { gap: var(--s-4); width: 100%; padding: var(--s-3) var(--s-4); - border: 1px solid #cfe0f5; + border: 1px solid var(--c-border); border-radius: var(--r-tile); background: var(--c-blue-soft); text-align: left; @@ -2327,7 +2343,7 @@ button.row-card:hover:not(:disabled) { place-items: center; width: 56px; height: 56px; - border-radius: 14px; + border-radius: var(--r-btn); background: var(--c-surface); color: var(--c-blue); line-height: 1; @@ -2335,7 +2351,7 @@ button.row-card:hover:not(:disabled) { .effect__num { font-size: var(--fs-24); - font-weight: 700; + font-weight: 600; } .effect__unit { @@ -2404,7 +2420,7 @@ button.row-card:hover:not(:disabled) { gap: var(--s-3); margin: 0; font-size: var(--fs-18); - font-weight: 700; + font-weight: 600; } .drug__sub { @@ -2443,7 +2459,7 @@ button.row-card:hover:not(:disabled) { border: 3px solid var(--c-surface); border-radius: 50%; background: var(--c-teal); - box-shadow: 0 1px 3px rgb(15 39 71 / 25%); + box-shadow: 0 1px 3px rgb(33 59 56 / 20%); transform: translate(-50%, -50%); } @@ -2463,7 +2479,7 @@ button.row-card:hover:not(:disabled) { @media (min-width: 1024px) { .drug__grid { - grid-template-columns: 1fr 1fr; + grid-template-columns: minmax(0, 1fr); gap: var(--s-5); } @@ -2498,10 +2514,12 @@ button.row-card:hover:not(:disabled) { min-height: 100dvh; margin: 0 auto; padding: var(--s-10) var(--s-4); - align-content: center; + align-content: start; } .app--bare .screen > .sosblock { + border: 1px solid var(--c-red-line); + border-left: 3px solid var(--c-red); display: grid; } @@ -2527,7 +2545,7 @@ button.row-card:hover:not(:disabled) { .result__title { font-size: var(--fs-20); - font-weight: 700; + font-weight: 600; } .result__chips { @@ -2621,9 +2639,9 @@ button.row-card:hover:not(:disabled) { gap: var(--s-3); padding: var(--s-3) var(--s-3) var(--s-3) var(--s-4); border-radius: var(--r-tile); - background: var(--c-navy); + background: var(--c-primary); color: #fff; - box-shadow: 0 12px 32px rgb(15 39 71 / 0.25); + box-shadow: 0 12px 32px rgb(33 59 56 / 0.15); pointer-events: auto; animation: toast-in 200ms var(--ease); } @@ -2656,3 +2674,266 @@ button.row-card:hover:not(:disabled) { transform: translateY(-8px); } } + +/* ---------------------------------------------------------------- workspace frame */ + +.app__main, +.screen, +.screen > *, +.split > *, +.cols > *, +.dash > * { + min-width: 0; +} + +.workspace { + grid-column: 1 / -1; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: var(--s-3); + margin: 0 max(var(--s-5), var(--safe-left)); + padding: calc(var(--s-4) + var(--safe-top)) 0 var(--s-4); + border-bottom: 1px solid var(--c-border); + color: var(--c-text-3); + font-size: var(--fs-13); +} + +.workspace__brand, +.workspace__profile { + display: inline-flex; + align-items: center; + gap: var(--s-2); +} + +.workspace__brand svg { + flex: none; +} + +.workspace__profile { + text-decoration: none; + color: var(--c-text); + font-weight: 500; +} + +.workspace__profile .avatar { + width: 34px; + height: 34px; + background: var(--c-teal-soft); + color: var(--c-primary); +} + +.workspace__tag { + font-size: var(--fs-12); +} + +.screen { + padding-top: var(--s-6); +} + +.head { + padding-bottom: var(--s-4); + border-bottom: 1px solid var(--c-border); +} + +.focus .head { + border-bottom: 0; + padding-bottom: 0; +} + +.tabbar__item.active { + background: var(--c-primary-soft); + box-shadow: inset 0 2px var(--c-primary); +} + +.btn--outline { + border-color: var(--c-border-strong); +} + +.btn--outline:hover:not(:disabled), +.btn--ghost:hover:not(:disabled), +.btn--soft:hover:not(:disabled), +.task:hover:not(:disabled), +.quick__btn:hover:not(:disabled) { + background: var(--c-primary-soft); + border-color: var(--c-border-strong); +} + +.composer:focus-within { + outline: 3px solid var(--c-focus); + outline-offset: 2px; +} + +.composer textarea { + min-width: 0; +} + +.log__row { + border: 0; + background: transparent; +} + +.log__row:hover:not(:disabled) { + background: var(--c-tile); +} + +.result { + border: 1px solid var(--c-border-strong); + border-left: 3px solid var(--c-teal); +} + +.notice--info { + border: 1px solid var(--c-border); +} + +.notice--warn, +.notice--danger { + border: 1px solid var(--c-red-line); + border-left: 3px solid var(--c-red); +} + +@media (max-width: 1023px) { + .screen > .stack, + .focus, + .done, + .dash__main { + padding: var(--s-5); + background: var(--c-surface); + border: 1px solid var(--c-border); + border-radius: var(--r-tile); + } + + .workspace { + max-width: 600px; + margin-inline: auto; + width: calc(100% - 40px); + } + + .workspace__tag { + display: none; + } +} + +@media (max-width: 480px) { + .screen > .stack, + .focus, + .done, + .dash__main { + padding: var(--s-4); + } + + .btn-row, + .quick, + .sosblock__lines { + grid-template-columns: minmax(0, 1fr); + } + + .signin__card { + padding: var(--s-6) var(--s-5); + } + + .task, + .effect { + flex-wrap: wrap; + } + + .task__text { + flex-basis: 55%; + } + + .signin__foot { + flex-wrap: wrap; + } + + .numscale__row { + gap: 3px; + } +} + +@media (min-width: 1024px) { + .workspace { + margin: 0; + min-height: 62px; + padding: 0 0 var(--s-5); + } + + .workspace:not(.workspace--bare) .workspace__brand svg { + display: none; + } + + .screen { + padding-top: 0; + } + + .sidebar { + display: flex; + flex-direction: column; + padding: var(--s-8) var(--s-5); + overflow-y: auto; + } + + .sidebar__brand > svg { + flex: none; + } + + .sidebar__note { + display: grid; + gap: var(--s-2); + margin-top: auto; + padding-top: var(--s-5); + border-top: 1px solid var(--c-border); + font-size: var(--fs-12); + line-height: 1.6; + color: var(--c-text-3); + } + + .sidebar__note strong { + color: var(--c-text); + font-weight: 500; + } + + .rail { + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + } + + .rail > * { + align-self: start; + } + + .screen > .stack, + .focus, + .done { + padding: var(--s-6); + } +} + +@media (min-width: 1200px) { + .dash { + grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); + } + + .grid2, + .cols, + .drug__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (min-width: 1440px) { + .app__main { + grid-template-columns: minmax(0, 1fr) var(--rail-w); + } + + .rail { + position: sticky; + grid-template-columns: minmax(0, 1fr); + } +} + +/* Respect reduced motion for toasts defined after the earlier component rules. */ +@media (prefers-reduced-motion: reduce) { + .toast { + animation: none; + } +} diff --git a/web/src/theme/tokens.css b/web/src/theme/tokens.css index cc72e7b..ebb5c01 100644 --- a/web/src/theme/tokens.css +++ b/web/src/theme/tokens.css @@ -1,45 +1,45 @@ /* - * Design tokens — single source of colours, type and spacing (docs/mock, series 2). + * Shared visual language with dashboard/theme.py and the approved doctor workspace. * Capacitor wraps this same bundle; a React Native port maps these 1:1 to a theme object. */ :root { /* Ink & surfaces */ - --c-navy: #0f2747; /* primary buttons, headings, desktop sidebar */ - --c-navy-hover: #0a1d37; - --c-navy-soft: #e8eef6; - --c-text: #13294b; - --c-text-2: #4d5d73; - --c-text-3: #7a889b; - --c-page: #ffffff; - --c-page-desk: #f4f7fb; /* desktop canvas behind white panels */ + --c-primary: #285e4e; /* primary buttons and active navigation */ + --c-primary-hover: #204b3e; + --c-primary-soft: #edf3ee; + --c-text: #213b38; + --c-text-2: #526660; + --c-text-3: #63736f; + --c-page: #f6f7f3; + --c-page-desk: #f6f7f3; /* desktop canvas behind white panels */ --c-surface: #ffffff; - --c-tile: #f3f7fb; /* soft card fill */ - --c-border: #e2e9f1; - --c-border-strong: #c9d4e1; + --c-tile: #f4f6f2; /* soft card fill */ + --c-border: #e1e7e3; + --c-border-strong: #b4c6ba; - /* Selection & positive: teal */ - --c-teal: #17a08f; - --c-teal-strong: #0e7e71; - --c-teal-soft: #e3f5f2; + /* Selection & positive */ + --c-teal: #527f61; + --c-teal-strong: #38634b; + --c-teal-soft: #eef4ee; - /* Info: blue */ - --c-blue: #2f7fdb; - --c-blue-soft: #eaf3fd; + /* Informational surfaces */ + --c-blue: #38634b; + --c-blue-soft: #edf3ee; /* Help & crisis: red */ - --c-red: #e2383b; - --c-red-strong: #c62b2e; - --c-red-soft: #fdeded; - --c-red-line: #f7d3d3; + --c-red: #a83236; + --c-red-strong: #a83236; + --c-red-soft: #faeeee; + --c-red-line: #e7cbcb; /* Warning */ - --c-amber: #e69a12; - --c-amber-soft: #fff4dd; + --c-amber: #805714; + --c-amber-soft: #fbf3e4; - --c-focus: #2f7fdb; + --c-focus: #598d7a; - /* Type: Golos Text (OFL), bundled locally — works offline. */ - --font: "Golos Text", system-ui, -apple-system, "Segoe UI", sans-serif; + /* Same offline system stack as the doctor workspace. */ + --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; --fs-12: 0.75rem; --fs-13: 0.8125rem; --fs-14: 0.875rem; @@ -63,14 +63,14 @@ --s-12: 48px; /* Radii: tiles > buttons > chips */ - --r-tile: 16px; - --r-btn: 12px; - --r-chip: 999px; + --r-tile: 12px; + --r-btn: 8px; + --r-chip: 5px; --tap: 52px; --nav-h: 64px; - --sidebar-w: 232px; - --rail-w: 300px; + --sidebar-w: 228px; + --rail-w: 264px; --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); From a4ee3d25f070ef5d2cb844298e441b5b59eddc8e Mon Sep 17 00:00:00 2001 From: Pavel Eliseev Date: Sun, 4 Oct 2026 16:14:37 +0300 Subject: [PATCH 2/2] fix(web): allow returning home from questionnaires Keep the shared dialog and submitted answers while leaving the focus screen. Remove the history trap and wire the text-step home action. Refs #85 --- web/src/content/ru.ts | 1 + web/src/screens/Flow.tsx | 29 ++++++++++++++--------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/web/src/content/ru.ts b/web/src/content/ru.ts index 37e1b28..1810915 100644 --- a/web/src/content/ru.ts +++ b/web/src/content/ru.ts @@ -204,6 +204,7 @@ export const ru = { toIntake: "Пройти первичный опрос", }, flow: { + home: "На главный экран", question: (n: number, total: number) => `Вопрос ${n} из ${total}`, period: "За последний месяц", textPlaceholder: "Напишите несколько слов", diff --git a/web/src/screens/Flow.tsx b/web/src/screens/Flow.tsx index 50d15fb..8613b56 100644 --- a/web/src/screens/Flow.tsx +++ b/web/src/screens/Flow.tsx @@ -265,7 +265,7 @@ function FreeText({ {skip.label} ) : ( - )} @@ -362,6 +362,7 @@ function SurveyAct({ press, say, onPause, + onHome, }: { step: Survey; stepKey: string; @@ -369,6 +370,7 @@ function SurveyAct({ press: (b: DialogButton) => void; say: (text: string) => void; onPause: (b: DialogButton) => void; + onHome: () => void; }) { let answer: ReactNode; switch (step.input) { @@ -381,7 +383,7 @@ function SurveyAct({ break; case "text": answer = ( - undefined} /> + ); break; default: @@ -522,17 +524,6 @@ export function Flow() { navigate("/today", { replace: true }); }, [step, stepKey, navigate, toast]); - // Questionnaire: answers are saved on the server per question, and there is no going back to change one. - // The browser «back» is absorbed by a duplicate history entry of this same screen. - const testing = step?.kind === "survey" || step?.kind === "choice" || step?.kind === "consent"; - useEffect(() => { - if (!testing) return; - window.history.pushState(null, "", window.location.href); - const stay = () => window.history.pushState(null, "", window.location.href); - window.addEventListener("popstate", stay); - return () => window.removeEventListener("popstate", stay); - }, [testing]); - const press = (b: DialogButton) => void dialog.send({ type: "button", button: b.data }, b.label); // «Продолжить позже»: answers are already saved on the server — pause and go straight home, no confirmation. const pause = (b: DialogButton) => @@ -588,7 +579,7 @@ export function Flow() { ) : item ? ( ) : null; - const top = {progress}; + const top = progress ? {progress} : null; // Phone: one column. Desktop: question and context on the left, answers and actions on the right. let lead: ReactNode; let act: ReactNode; @@ -702,7 +693,7 @@ export function Flow() { break; case "survey": lead = ; - act = ; + act = ; break; case "choice": lead = ; @@ -725,6 +716,14 @@ export function Flow() { return ( + {content} );