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'
Покажите 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 (
+
+ );
+}
+
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..1810915 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: "Сегодня",
@@ -201,6 +204,7 @@ export const ru = {
toIntake: "Пройти первичный опрос",
},
flow: {
+ home: "На главный экран",
question: (n: number, total: number) => `Вопрос ${n} из ${total}`,
period: "За последний месяц",
textPlaceholder: "Напишите несколько слов",
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/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}
) : (
-