feat(miniapp): premium redesign — gradient profile card, SVG icons, native-style grouped menus, dark theme

This commit is contained in:
wasrusgen 2026-05-09 01:22:30 +03:00
parent 6b0b01e15e
commit 57eefbbf5c
4 changed files with 582 additions and 150 deletions

View File

@ -1,24 +1,51 @@
// ЗОВ MiniApp — главный скрипт // ЗОВ MiniApp — главный скрипт.
// На входе: подписанный initData от Telegram. // На входе: подписанный initData от Telegram.
// Ходим на backend → получаем профиль (роль, статус) → рендерим меню. // Ходим на backend → получаем профиль (роль, статус) → рендерим меню.
const tg = window.Telegram?.WebApp; const tg = window.Telegram?.WebApp;
const BACKEND_URL = ""; // TODO: заполнить URL Apps Script Web App const BACKEND_URL = ""; // TODO: заполнить URL Apps Script Web App после Шага 4
const app = document.getElementById("app"); const app = document.getElementById("app");
/* ----------------- Telegram WebApp setup ----------------- */
function setupTelegram() {
if (!tg) return;
try {
tg.ready();
tg.expand();
if (tg.setHeaderColor) tg.setHeaderColor("#003E7E");
if (tg.setBackgroundColor) tg.setBackgroundColor("#F4F4F5");
if (tg.enableClosingConfirmation) tg.enableClosingConfirmation();
} catch (e) { console.warn(e); }
}
function haptic(type = "selection") {
try {
if (!tg?.HapticFeedback) return;
if (type === "impact") tg.HapticFeedback.impactOccurred("light");
else if (type === "success") tg.HapticFeedback.notificationOccurred("success");
else tg.HapticFeedback.selectionChanged();
} catch (e) {}
}
/* ----------------- Data ----------------- */
async function fetchMe() { async function fetchMe() {
if (!BACKEND_URL) { if (!BACKEND_URL) {
// dev-режим без backend — для локального просмотра вёрстки // dev-режим без backend — мок для просмотра вёрстки
return { return {
role: "manager", role: "manager",
user: { full_name: "Тест Менеджер", salon: "ЗОВ Москва" }, user: {
full_name: "Руслан Васильев",
salon: "ЗОВ Москва",
avatar_initial: "Р",
},
status: "active", status: "active",
status_until: "2026-08-12", status_until: "12.08.2026",
}; };
} }
const res = await fetch(`${BACKEND_URL}/api/me`, { const res = await fetch(`${BACKEND_URL}/api/me`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
initData: tg?.initData || "", initData: tg?.initData || "",
startParam: tg?.initDataUnsafe?.start_param || null, startParam: tg?.initDataUnsafe?.start_param || null,
@ -27,92 +54,172 @@ async function fetchMe() {
return res.json(); return res.json();
} }
function renderManager(me) { /* ----------------- Helpers ----------------- */
const status = me.status || "active"; function el(html) {
app.innerHTML = ` const t = document.createElement("template");
<div class="header"> t.innerHTML = html.trim();
<h1>Кабинет менеджера</h1> return t.content.firstChild;
<div class="subtitle">
${me.user.full_name} · ${me.user.salon || ""}
<span class="status-badge ${status}">${statusLabel(status)}</span>
</div>
</div>
<nav class="menu">
<a class="menu-item" href="#/m/podbor">
<span class="icon">🔧</span>
<span class="label">Подбор техники для клиента</span>
<span class="arrow"></span>
</a>
<a class="menu-item" href="#/m/measurements">
<span class="icon">📐</span>
<span class="label">Замеры</span>
<span class="arrow"></span>
</a>
<div class="menu-item disabled">
<span class="icon">📋</span>
<span class="label">Заявки клиентов <small>(скоро)</small></span>
</div>
<div class="menu-item disabled">
<span class="icon">💼</span>
<span class="label">Сделки <small>(скоро)</small></span>
</div>
<a class="menu-item" href="#/m/status">
<span class="icon">💰</span>
<span class="label">Мой статус и доступ</span>
<span class="arrow"></span>
</a>
</nav>
`;
}
function renderClient(me) {
app.innerHTML = `
<div class="header">
<h1>Кабинет клиента</h1>
<div class="subtitle">
${me.user.full_name || "Здравствуйте!"}
${me.manager ? `<br>Менеджер: ${me.manager.full_name}, ${me.manager.salon || ""}` : ""}
</div>
</div>
<nav class="menu">
<a class="menu-item" href="#/c/measure">
<span class="icon">📐</span>
<span class="label">Замер кухни</span>
<span class="arrow"></span>
</a>
<div class="menu-item disabled">
<span class="icon">🔧</span>
<span class="label">Подобрать технику <small>(скоро)</small></span>
</div>
<div class="menu-item disabled">
<span class="icon">💡</span>
<span class="label">Идеи и кейсы <small>(скоро)</small></span>
</div>
<a class="menu-item" href="#/c/contact">
<span class="icon">📞</span>
<span class="label">Связаться с менеджером</span>
<span class="arrow"></span>
</a>
</nav>
`;
} }
function statusLabel(s) { function statusLabel(s) {
return { active: "🟢 active", lapsed: "🔴 lapsed", grace: "🟡 grace" }[s] || s; return ({
active: "Доступ открыт",
lapsed: "Доступ ограничен",
grace: "Грейс-период",
})[s] || s;
} }
function getInitial(name) {
return (name || "").trim().slice(0, 1).toUpperCase() || "?";
}
/* ----------------- Renders ----------------- */
function renderManager(me) {
const status = me.status || "active";
const statusUntil = me.status_until ? `до ${me.status_until}` : "";
const initial = me.user?.avatar_initial || getInitial(me.user?.full_name);
app.innerHTML = "";
app.appendChild(el(`
<header class="profile-card">
<div class="avatar">${initial}</div>
<div class="info">
<div class="role-tag">Менеджер</div>
<div class="name">${me.user?.full_name || ""}</div>
<div class="meta">${me.user?.salon || ""}</div>
<span class="status-row">
<span class="status-dot ${status}"></span>
<span>${statusLabel(status)}${statusUntil ? " · " + statusUntil : ""}</span>
</span>
</div>
</header>
`));
const sections = [
{
label: "Работа с клиентами",
items: [
{ icon: "wrench", color: "green", label: "Подбор техники для клиента", href: "#/m/podbor" },
{ icon: "ruler", color: "blue", label: "Замеры", href: "#/m/measurements" },
{ icon: "clipboard", color: "gold", label: "Заявки клиентов", soon: true },
{ icon: "briefcase", color: "gray", label: "Сделки", soon: true },
],
},
{
label: "Аккаунт",
items: [
{ icon: "wallet", color: "gold", label: "Мой статус и доступ", href: "#/m/status" },
{ icon: "help", color: "blue", label: "Связь с куратором", href: "#/m/help" },
],
},
];
sections.forEach(section => {
app.appendChild(el(`<div class="section-label">${section.label}</div>`));
app.appendChild(buildMenu(section.items));
});
app.appendChild(el(`
<div class="footer-hint">
Куратор партнёрской сети Руслан Васильев<br>
<a href="https://t.me/wasrusgen">@wasrusgen</a>
</div>
`));
}
function renderClient(me) {
const initial = me.user?.avatar_initial || getInitial(me.user?.full_name) || "?";
const greetName = me.user?.full_name || "Здравствуйте";
app.innerHTML = "";
app.appendChild(el(`
<header class="profile-card">
<div class="avatar">${initial}</div>
<div class="info">
<div class="role-tag">Клиент</div>
<div class="name">${greetName}</div>
<div class="meta">${me.manager ? "Менеджер: " + me.manager.full_name + (me.manager.salon ? ", " + me.manager.salon : "") : "ЗОВ — кухонная мебель"}</div>
</div>
</header>
`));
const sections = [
{
label: "Подобрать кухню",
items: [
{ icon: "ruler", color: "blue", label: "Замер кухни", href: "#/c/measure" },
{ icon: "wrench", color: "green", label: "Подобрать технику", soon: true },
{ icon: "wallet", color: "gold", label: "Калькулятор бюджета", soon: true },
],
},
{
label: "Помощь",
items: [
{ icon: "lightbulb", color: "gold", label: "Идеи и кейсы", soon: true },
{ icon: "phone", color: "blue", label: "Связаться с менеджером", href: "#/c/contact" },
{ icon: "pin", color: "green", label: "Записаться в салон", soon: true },
],
},
];
sections.forEach(section => {
app.appendChild(el(`<div class="section-label">${section.label}</div>`));
app.appendChild(buildMenu(section.items));
});
app.appendChild(el(`
<div class="footer-hint">
Фабрика кухонной мебели <strong>ЗОВ</strong><br>
<a href="https://t.me/wasrusgen1">канал @wasrusgen1</a>
</div>
`));
}
function buildMenu(items) {
const menu = el(`<nav class="menu"></nav>`);
items.forEach(item => {
const cls = item.soon ? "menu-item disabled" : "menu-item";
const node = el(`
<a class="${cls}" ${item.href && !item.soon ? `href="${item.href}"` : ""}>
<div class="icon ${item.color}">${ICONS[item.icon] || ""}</div>
<div class="text">
<div class="label">
${item.label}
${item.soon ? '<span class="badge">скоро</span>' : ""}
</div>
${item.sub ? `<div class="sub">${item.sub}</div>` : ""}
</div>
${item.soon ? "" : `<div class="chevron">${ICONS.chevron}</div>`}
</a>
`);
if (!item.soon) node.addEventListener("click", () => haptic("impact"));
menu.appendChild(node);
});
return menu;
}
function renderError() {
app.innerHTML = "";
app.appendChild(el(`
<div class="error">
<h3>Не удалось загрузить кабинет</h3>
<div>Проверьте подключение и попробуйте позже.</div>
</div>
`));
}
/* ----------------- Init ----------------- */
async function init() { async function init() {
if (tg) { setupTelegram();
tg.ready();
tg.expand();
}
try { try {
const me = await fetchMe(); const me = await fetchMe();
if (me.role === "manager") renderManager(me); if (me.role === "manager") renderManager(me);
else renderClient(me); else renderClient(me);
} catch (e) { } catch (e) {
app.innerHTML = `<div class="loader">Ошибка загрузки. Попробуйте позже.</div>`;
console.error(e); console.error(e);
renderError();
} }
} }

30
miniapp/assets/icons.js Normal file
View File

@ -0,0 +1,30 @@
// Lucide-style line icons (24x24, stroke 2). Используем currentColor.
// MIT-аналоги, перерисованы вручную для облегчения веса.
const ICONS = {
wrench: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
ruler: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.4 2.4 0 0 1 0-3.4l2.6-2.6a2.4 2.4 0 0 1 3.4 0Z"/><path d="M14.5 12.5 12 15"/><path d="M11.5 9.5 9 12"/><path d="M8.5 6.5 6 9"/><path d="M17.5 15.5 15 18"/></svg>`,
clipboard: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>`,
briefcase: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/><rect width="20" height="14" x="2" y="6" rx="2"/></svg>`,
wallet: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4"/><path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/></svg>`,
phone: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`,
home: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>`,
lightbulb: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>`,
pin: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg>`,
calendar: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></svg>`,
help: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>`,
user: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.2"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>`,
chevron: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.2"><path d="m9 6 6 6-6 6"/></svg>`,
};

View File

@ -1,107 +1,398 @@
/* ============================================================
ЗОВ MiniApp design system v2
============================================================ */
:root { :root {
--bronze: #76BD22; /* Brand */
--accent: #003E7E; --brand-green: #76BD22;
--cream: #FAFAFA; --brand-green-dark: #5FA316;
--sand: #F0F9E8; --brand-blue: #003E7E;
--ink: #1B1B1B; --brand-blue-dark: #002952;
--muted: #6B6B6B; --brand-cream: #FAF8F3;
--line: #E5E5E5; --brand-gold: #C9A95E;
--r: 12px; /* Neutral palette adapts to Telegram theme */
--shadow: 0 1px 3px rgba(0, 0, 0, 0.06); --bg: var(--tg-theme-bg-color, #FFFFFF);
--bg-secondary: var(--tg-theme-secondary-bg-color, #F4F4F5);
--bg-section: var(--tg-theme-section-bg-color, #FFFFFF);
--text: var(--tg-theme-text-color, #0F0F0F);
--text-muted: var(--tg-theme-hint-color, #707579);
--text-section: var(--tg-theme-section-header-text-color, #707579);
--link: var(--tg-theme-link-color, #003E7E);
--line: rgba(0, 0, 0, 0.08);
/* Status */
--status-active: #76BD22;
--status-active-bg: #EEF7E0;
--status-lapsed: #DC2626;
--status-lapsed-bg: #FEE2E2;
--status-grace: #D97706;
--status-grace-bg: #FEF3C7;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 8px 32px rgba(0, 62, 126, 0.18);
/* Radii */
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
--r-xl: 20px;
/* Spacing */
--s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px;
--s5: 20px; --s6: 24px; --s7: 28px; --s8: 32px;
/* Type */
--font-body: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
} }
/* Dark theme overrides */
@media (prefers-color-scheme: dark) {
:root {
--line: rgba(255, 255, 255, 0.10);
--status-active-bg: rgba(118, 189, 34, 0.15);
--status-lapsed-bg: rgba(220, 38, 38, 0.15);
--status-grace-bg: rgba(217, 119, 6, 0.15);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
}
}
/* ============================================================
Reset
============================================================ */
* { box-sizing: border-box; } * { box-sizing: border-box; }
* { -webkit-tap-highlight-color: transparent; }
html, body { margin: 0; padding: 0; }
html, body { body {
margin: 0; font-family: var(--font-body);
padding: 0; font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.4;
color: var(--ink); color: var(--text);
background: var(--cream); background: var(--bg-secondary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
} }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
/* ============================================================
Layout
============================================================ */
#app { #app {
max-width: 720px; max-width: 560px;
margin: 0 auto; margin: 0 auto;
padding: 16px; padding: var(--s4);
padding-bottom: calc(var(--s8) + env(safe-area-inset-bottom));
min-height: 100vh; min-height: 100vh;
} }
/* ============================================================
Loader
============================================================ */
.loader { .loader {
text-align: center;
padding: 64px 16px;
color: var(--muted);
}
.header {
margin-bottom: 16px;
}
.header h1 {
font-size: 20px;
margin: 0 0 4px;
color: var(--accent);
}
.header .subtitle {
font-size: 14px;
color: var(--muted);
}
.menu {
display: grid; display: grid;
gap: 8px; place-items: center;
min-height: 60vh;
} }
.menu-item { .spinner {
width: 36px;
height: 36px;
border: 3px solid var(--line);
border-top-color: var(--brand-blue);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ============================================================
Profile card (header)
============================================================ */
.profile-card {
background: linear-gradient(135deg, var(--brand-blue) 0%, var(--brand-blue-dark) 100%);
color: #FFFFFF;
border-radius: var(--r-lg);
padding: var(--s5);
margin-bottom: var(--s5);
box-shadow: var(--shadow-lg);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: var(--s4);
padding: 16px; position: relative;
background: white; overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--r);
cursor: pointer;
user-select: none;
text-decoration: none;
color: inherit;
transition: transform 0.05s, box-shadow 0.1s;
} }
.menu-item:active { .profile-card::before {
transform: scale(0.99); content: "";
box-shadow: var(--shadow); position: absolute;
right: -40px;
top: -40px;
width: 160px;
height: 160px;
background: var(--brand-green);
opacity: 0.18;
border-radius: 50%;
filter: blur(8px);
} }
.menu-item.disabled { .profile-card::after {
opacity: 0.4; content: "";
cursor: not-allowed; position: absolute;
right: 16px;
bottom: -20px;
width: 80px;
height: 80px;
background: var(--brand-gold);
opacity: 0.12;
border-radius: 50%;
} }
.menu-item .icon { .profile-card .avatar {
font-size: 22px; width: 56px;
height: 56px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(8px);
display: grid;
place-items: center;
flex-shrink: 0; flex-shrink: 0;
font-size: 24px;
font-weight: 600;
color: #FFFFFF;
position: relative;
z-index: 1;
} }
.menu-item .label { .profile-card .info {
flex: 1; flex: 1;
font-weight: 500; min-width: 0;
position: relative;
z-index: 1;
} }
.menu-item .arrow { .profile-card .role-tag {
color: var(--muted); font-size: 11px;
font-size: 18px; font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
opacity: 0.7;
margin-bottom: 2px;
} }
.status-badge { .profile-card .name {
display: inline-block; font-size: 19px;
padding: 2px 8px; font-weight: 600;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.profile-card .meta {
font-size: 13px;
opacity: 0.85;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.profile-card .status-row {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: var(--s2);
padding: 4px 10px 4px 6px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(8px);
border-radius: 999px; border-radius: 999px;
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;
} }
.status-badge.active { background: var(--sand); color: var(--bronze); } .profile-card .status-dot {
.status-badge.lapsed { background: #FFF1F0; color: #C82C2C; } width: 8px;
.status-badge.grace { background: #FFF8E1; color: #B07E00; } height: 8px;
border-radius: 50%;
background: var(--brand-green);
box-shadow: 0 0 0 3px rgba(118, 189, 34, 0.4);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 3px rgba(118, 189, 34, 0.4); }
50% { box-shadow: 0 0 0 6px rgba(118, 189, 34, 0.0); }
}
.profile-card .status-dot.lapsed { background: #FCA5A5; box-shadow: 0 0 0 3px rgba(252, 165, 165, 0.4); animation: none; }
.profile-card .status-dot.grace { background: #FCD34D; box-shadow: 0 0 0 3px rgba(252, 211, 77, 0.4); }
/* ============================================================
Section label
============================================================ */
.section-label {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-section);
padding: 0 var(--s4) var(--s2);
margin-top: var(--s5);
}
.section-label:first-child { margin-top: 0; }
/* ============================================================
Menu (grouped list)
============================================================ */
.menu {
background: var(--bg-section);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.menu + .menu { margin-top: var(--s4); }
.menu-item {
display: flex;
align-items: center;
gap: var(--s3);
padding: var(--s3) var(--s4);
min-height: 64px;
color: var(--text);
cursor: pointer;
transition: background 0.15s;
position: relative;
}
.menu-item + .menu-item::before {
content: "";
position: absolute;
top: 0;
left: 64px;
right: 0;
height: 1px;
background: var(--line);
}
.menu-item:active {
background: var(--bg-secondary);
}
.menu-item .icon {
width: 36px;
height: 36px;
border-radius: 9px;
display: grid;
place-items: center;
flex-shrink: 0;
}
.menu-item .icon svg {
width: 20px;
height: 20px;
stroke-width: 2;
}
/* Icon variants */
.menu-item .icon.green { background: #EEF7E0; color: #5FA316; }
.menu-item .icon.blue { background: #E5EDF5; color: #003E7E; }
.menu-item .icon.gold { background: #FAF1DC; color: #BF8A2E; }
.menu-item .icon.gray { background: var(--bg-secondary); color: var(--text-muted); }
.menu-item .icon.red { background: #FEE2E2; color: #DC2626; }
@media (prefers-color-scheme: dark) {
.menu-item .icon.green { background: rgba(118, 189, 34, 0.15); color: #A4D85F; }
.menu-item .icon.blue { background: rgba(0, 62, 126, 0.25); color: #6FA0D6; }
.menu-item .icon.gold { background: rgba(201, 169, 94, 0.15); color: #E0C079; }
.menu-item .icon.red { background: rgba(220, 38, 38, 0.15); color: #F87171; }
}
.menu-item .text {
flex: 1;
min-width: 0;
}
.menu-item .label {
font-size: 15.5px;
font-weight: 500;
letter-spacing: -0.01em;
display: flex;
align-items: center;
gap: var(--s2);
}
.menu-item .sub {
font-size: 13px;
color: var(--text-muted);
margin-top: 2px;
}
.menu-item .chevron {
color: var(--text-muted);
flex-shrink: 0;
opacity: 0.5;
}
.menu-item.disabled {
cursor: not-allowed;
}
.menu-item.disabled .label,
.menu-item.disabled .icon {
opacity: 0.45;
}
.badge {
display: inline-block;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 2px 7px;
border-radius: 999px;
background: var(--bg-secondary);
color: var(--text-muted);
vertical-align: 1px;
}
/* ============================================================
Footer hint
============================================================ */
.footer-hint {
text-align: center;
font-size: 12px;
color: var(--text-muted);
margin-top: var(--s6);
padding: 0 var(--s4);
line-height: 1.5;
}
.footer-hint a { color: var(--link); }
/* ============================================================
Error state
============================================================ */
.error {
background: var(--bg-section);
border-radius: var(--r-md);
padding: var(--s5);
text-align: center;
color: var(--text-muted);
box-shadow: var(--shadow-sm);
}
.error h3 {
margin: 0 0 var(--s2);
color: var(--text);
font-size: 17px;
}

View File

@ -2,15 +2,19 @@
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, user-scalable=no">
<meta name="theme-color" content="#003E7E">
<title>ЗОВ — Кабинет</title> <title>ЗОВ — Кабинет</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script> <script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="assets/styles.css"> <link rel="stylesheet" href="assets/styles.css">
</head> </head>
<body> <body>
<main id="app"> <main id="app">
<div class="loader">Загрузка...</div> <div class="loader">
<div class="spinner"></div>
</div>
</main> </main>
<script src="assets/icons.js"></script>
<script src="assets/app.js"></script> <script src="assets/app.js"></script>
</body> </body>
</html> </html>