From 58ff6ff6144d6ab86cb6128edbec1f7ec5c4b16b Mon Sep 17 00:00:00 2001 From: VPN SaaS Dev Date: Tue, 19 May 2026 05:05:24 +0900 Subject: [PATCH] compact UI and add localization switch --- web/book_sto.html | 27 ++-- web/index.html | 134 ++++++++++------- web/static/app.js | 93 +++++++++++- web/static/book_sto.js | 10 ++ web/static/page_common.js | 298 +++++++++++++++++++++++++++++++++++++- web/static/styles.css | 264 ++++++++++++++++++++++++++++++++- 6 files changed, 761 insertions(+), 65 deletions(-) diff --git a/web/book_sto.html b/web/book_sto.html index 434152a..e8ff6a2 100644 --- a/web/book_sto.html +++ b/web/book_sto.html @@ -53,15 +53,18 @@
- - - +
+ Фильтры + + + +
@@ -89,6 +92,12 @@ +
+ + + + +
- - +
+ Напоминание о следующем ТО +
+ + +
+
@@ -292,6 +297,28 @@
+ + diff --git a/web/static/app.js b/web/static/app.js index 6bf459d..fddcb2c 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -4,6 +4,8 @@ tg?.expand(); const textNodes = new WeakMap(); const attrOriginals = new WeakMap(); +let translationObserver = null; +let translationTimer = null; const i18n = { en: { @@ -83,6 +85,14 @@ const i18n = { "Марка": "Make", "Модель": "Model", "Добавить авто": "Add vehicle", + "Добавить запись": "Add entry", + "Расход": "Expense", + "дата, пробег, литры, цена": "date, odometer, liters, price", + "работа, стоимость, следующий срок": "work, cost, next due", + "страховка, штраф, парковка, прочее": "insurance, fine, parking, other", + "фото или файл": "photo or file", + "Дополнительно": "More options", + "Напоминание о следующем ТО": "Next maintenance reminder", "За весь срок": "All time", "За месяц": "This month", "За день": "Per day", @@ -215,6 +225,14 @@ const i18n = { "Марка": "브랜드", "Модель": "모델", "Добавить авто": "차량 추가", + "Добавить запись": "기록 추가", + "Расход": "지출", + "дата, пробег, литры, цена": "날짜, 주행거리, 리터, 가격", + "работа, стоимость, следующий срок": "작업, 비용, 다음 예정", + "страховка, штраф, парковка, прочее": "보험, 벌금, 주차, 기타", + "фото или файл": "사진 또는 파일", + "Дополнительно": "추가 옵션", + "Напоминание о следующем ТО": "다음 정비 알림", "За весь срок": "전체", "За месяц": "월", "За день": "일 평균", @@ -304,6 +322,15 @@ function applyTranslations(root = document.body) { }); } +function observeTranslations(root = document.body) { + if (translationObserver || !root) return; + translationObserver = new MutationObserver(() => { + window.clearTimeout(translationTimer); + translationTimer = window.setTimeout(() => applyTranslations(root), 40); + }); + translationObserver.observe(root, { childList: true, subtree: true }); +} + const state = { user: null, @@ -582,6 +609,7 @@ async function ensureUser() { method: "POST", body: JSON.stringify({ init_data: tg.initData }), }); + localStorage.setItem("carpassLocale", state.user.locale || "ru"); hideAuthOverlay(); updateRoleVisibility(); return; @@ -590,6 +618,7 @@ async function ensureUser() { const devId = localStorage.getItem("driversDevTelegramId") || "1"; localStorage.setItem("driversDevTelegramId", devId); state.user = await api("/users/me"); + localStorage.setItem("carpassLocale", state.user.locale || "ru"); hideAuthOverlay(); updateRoleVisibility(); return; @@ -598,6 +627,36 @@ async function ensureUser() { throw new Error("Требуется вход через Telegram"); } +function installLocaleSwitch() { + const topActions = document.querySelector(".topbar .top-actions"); + if (!topActions || document.querySelector("#globalLocaleSelect")) return; + const select = document.createElement("select"); + select.id = "globalLocaleSelect"; + select.className = "locale-switch"; + select.setAttribute("aria-label", "Язык"); + select.innerHTML = ` + + + + `; + select.value = state.user?.locale || "ru"; + select.addEventListener("change", async () => { + await runAction(select, "Сохраняю...", async () => { + state.user = await api(`/users/${state.user.id}/preferences`, { + method: "PATCH", + body: JSON.stringify({ locale: select.value, currency: state.user.currency }), + }); + localStorage.setItem("carpassLocale", state.user.locale || "ru"); + document.querySelector("#localeSelect").value = state.user.locale || "ru"; + applyTranslations(); + renderCars(); + renderStats(state.latestStats); + toast("Сохранено"); + }); + }); + topActions.prepend(select); +} + function hideAuthOverlay() { document.querySelector("#authOverlay")?.classList.add("hidden"); document.body.classList.remove("auth-required"); @@ -2671,6 +2730,9 @@ document.querySelector("#settingsForm").addEventListener("submit", async (event) method: "PATCH", body: JSON.stringify({ locale: data.locale, currency: data.currency }), }); + localStorage.setItem("carpassLocale", state.user.locale || "ru"); + const globalLocale = document.querySelector("#globalLocaleSelect"); + if (globalLocale) globalLocale.value = state.user.locale || "ru"; applyTranslations(); initCarCatalog(); await loadSelectedCar(); @@ -2700,6 +2762,7 @@ document.querySelector("#fuelForm").addEventListener("submit", async (event) => }); form.reset(); form.entry_date.value = today(); + form.is_full_tank.checked = true; await loadSelectedCar(); toast("Сохранено"); haptic("success"); @@ -2802,6 +2865,20 @@ function mountEntryForms() { } } +function fillEntryDefaults(sectionId) { + const car = selectedCar(); + const odometer = car?.current_odometer || ""; + const sections = sectionId ? [document.querySelector(`#${sectionId}`)] : [...document.querySelectorAll(".drawer-section")]; + sections.filter(Boolean).forEach((section) => { + section.querySelectorAll('input[name="entry_date"]').forEach((input) => { + if (!input.value) input.value = today(); + }); + section.querySelectorAll('input[name="odometer"]').forEach((input) => { + if (!input.value && odometer) input.value = odometer; + }); + }); +} + async function openDrawerSection(sectionId, options = {}) { if (!canOpenDrawerSection(sectionId)) { toast("Этот раздел недоступен для вашей роли", "error"); @@ -2817,6 +2894,7 @@ async function openDrawerSection(sectionId, options = {}) { button.classList.toggle("active", button.dataset.menuSection === sectionId); }); mountEntryForms(); + fillEntryDefaults(sectionId); if (sectionId === "carProfileSection") fillCarProfileForm(); if (sectionId === "settingsSection") { document.querySelector("#localeSelect").value = state.user?.locale || "ru"; @@ -2891,7 +2969,18 @@ document.querySelector("#addCarQuickBtn").addEventListener("click", () => { }); document.querySelector("#addRecordPrimaryBtn").addEventListener("click", () => { - openDrawerSection("expensesSection"); + openDrawerSection("quickAddSection"); +}); + +document.querySelectorAll("[data-quick-entry]").forEach((button) => { + button.addEventListener("click", async () => { + haptic(); + if (button.dataset.quickEntry === "scan") { + openScanModal(); + return; + } + await openDrawerSection(button.dataset.quickEntry); + }); }); document.querySelectorAll("[data-menu-section]").forEach((button) => { @@ -3043,6 +3132,8 @@ initPwa(); Promise.all([loadAuthConfig()]) .then(() => Promise.all([ensureUser(), loadCatalog()])) .then(() => { + installLocaleSwitch(); + observeTranslations(); document.querySelector("#localeSelect").value = state.user?.locale || "ru"; document.querySelector("#currencySelect").value = state.user?.currency || "RUB"; document.querySelector("#expenseForm").currency.value = state.user?.currency || "RUB"; diff --git a/web/static/book_sto.js b/web/static/book_sto.js index d121e35..df8159a 100644 --- a/web/static/book_sto.js +++ b/web/static/book_sto.js @@ -113,6 +113,16 @@ document.querySelector("#filterForm").addEventListener("submit", async (event) = }); }); +document.querySelectorAll("[data-booking-service]").forEach((button) => { + button.addEventListener("click", async () => { + document.querySelector("#serviceTypeSelect").value = button.dataset.bookingService; + document.querySelectorAll("[data-booking-service]").forEach((item) => { + item.classList.toggle("active", item === button); + }); + await loadSlots().catch((error) => page.toast(error.message || "Не удалось обновить окна", "error")); + }); +}); + document.querySelector("#bookingForm").addEventListener("submit", async (event) => { event.preventDefault(); const center = selectedCenter(); diff --git a/web/static/page_common.js b/web/static/page_common.js index 49000b3..16a2d6f 100644 --- a/web/static/page_common.js +++ b/web/static/page_common.js @@ -2,6 +2,238 @@ const tg = window.Telegram?.WebApp; tg?.ready(); tg?.expand(); +const CarPassI18n = (() => { + const textNodes = new WeakMap(); + const attrOriginals = new WeakMap(); + let observer = null; + let timer = null; + const dictionaries = { + en: { + "Гараж": "Garage", + "Автомобили": "Vehicles", + "Автомобиль": "Vehicle", + "Авто": "Vehicles", + "Заправка": "Fuel", + "Сервис": "Service", + "Расход": "Expense", + "Расходы": "Expenses", + "ТО и ремонт": "Maintenance and repair", + "Дата": "Date", + "Одометр, км": "Odometer, km", + "Одометр": "Odometer", + "Литры": "Liters", + "Цена за литр": "Price per liter", + "АЗС": "Fuel station", + "Полный бак": "Full tank", + "Стоимость": "Cost", + "Валюта": "Currency", + "Категория": "Category", + "Название": "Title", + "Комментарий": "Comment", + "Сохранить": "Save", + "Сохранить запись": "Save entry", + "Сохранить расход": "Save expense", + "Сохранить заправку": "Save fuel entry", + "Сохранить настройки": "Save settings", + "Создать запись": "Create booking", + "Запись в СТО": "Book service", + "СТО": "Service centers", + "Сервисы": "Services", + "Каталог": "Catalog", + "Заявка": "Request", + "Выберите сервис": "Choose service", + "Город": "City", + "Специализация": "Specialization", + "Найти": "Search", + "Что нужно сделать": "What needs to be done", + "Услуга": "Service", + "Длительность": "Duration", + "Свободное окно": "Available slot", + "Окно записи": "Booking slot", + "Отправить заявку": "Send request", + "Проверить карточку авто": "Check vehicle card", + "Меню": "Menu", + "Проверить вход": "Check login", + "Открыть в Telegram": "Open in Telegram", + "Обновить": "Refresh", + "Настройки": "Settings", + "Язык": "Language", + "Панель СТО": "Service workplace", + "Записи клиентов": "Client bookings", + "Заказ-наряды": "Work orders", + "Сотрудники": "Staff", + "Пригласить": "Invite", + "Заказ-наряд": "Work order", + "Работы": "Labor", + "Запчасти и жидкости": "Parts and fluids", + "Проверьте смету": "Review estimate", + "Согласовать": "Approve", + "Отклонить": "Reject", + "Админ-панель": "Admin panel", + "Операционный обзор": "Operational overview", + "Последние события": "Latest events", + "Быстрые переходы": "Quick links", + "Заявки СТО": "Service applications", + "Записи": "Bookings", + "Фильтр": "Filter", + "Фильтры": "Filters", + "Запросить": "Query", + "Показать": "Show", + "Импорт и экспорт": "Import and export", + "Скачать JSON": "Download JSON", + "Проверить файл": "Preview file", + "Импортировать": "Import", + "Паспорт автомобиля": "Vehicle passport", + "Выберите автомобиль": "Choose vehicle", + "Параметры авто": "Vehicle settings", + "Сохранить паспорт": "Save passport", + "Удалить автомобиль": "Delete vehicle", + "Готов к работе": "Ready", + "Сохраняю...": "Saving...", + "Сохранено": "Saved", + "Ошибка": "Error", + "Нет данных": "No data", + "Нет доступа": "No access", + }, + ko: { + "Гараж": "차고", + "Автомобили": "차량", + "Автомобиль": "차량", + "Авто": "차량", + "Заправка": "주유", + "Сервис": "정비", + "Расход": "지출", + "Расходы": "지출", + "ТО и ремонт": "정비 및 수리", + "Дата": "날짜", + "Одометр, км": "주행거리, km", + "Одометр": "주행거리", + "Литры": "리터", + "Цена за литр": "리터당 가격", + "АЗС": "주유소", + "Полный бак": "가득 주유", + "Стоимость": "비용", + "Валюта": "통화", + "Категория": "카테고리", + "Название": "제목", + "Комментарий": "메모", + "Сохранить": "저장", + "Сохранить запись": "기록 저장", + "Сохранить расход": "지출 저장", + "Сохранить заправку": "주유 저장", + "Сохранить настройки": "설정 저장", + "Создать запись": "예약 생성", + "Запись в СТО": "정비소 예약", + "СТО": "정비소", + "Сервисы": "서비스", + "Каталог": "목록", + "Заявка": "요청", + "Выберите сервис": "정비소 선택", + "Город": "도시", + "Специализация": "전문 분야", + "Найти": "검색", + "Что нужно сделать": "필요한 작업", + "Услуга": "서비스", + "Длительность": "소요 시간", + "Свободное окно": "예약 가능 시간", + "Окно записи": "예약 시간", + "Отправить заявку": "요청 보내기", + "Проверить карточку авто": "차량 카드 확인", + "Меню": "메뉴", + "Проверить вход": "로그인 확인", + "Открыть в Telegram": "텔레그램에서 열기", + "Обновить": "새로고침", + "Настройки": "설정", + "Язык": "언어", + "Панель СТО": "정비소 작업실", + "Записи клиентов": "고객 예약", + "Заказ-наряды": "작업지시서", + "Сотрудники": "직원", + "Пригласить": "초대", + "Заказ-наряд": "작업지시서", + "Работы": "공임", + "Запчасти и жидкости": "부품 및 오일", + "Проверьте смету": "견적 확인", + "Согласовать": "승인", + "Отклонить": "거절", + "Админ-панель": "관리자 패널", + "Операционный обзор": "운영 요약", + "Последние события": "최근 이벤트", + "Быстрые переходы": "빠른 이동", + "Заявки СТО": "정비소 신청", + "Записи": "예약", + "Фильтр": "필터", + "Фильтры": "필터", + "Запросить": "조회", + "Показать": "보기", + "Импорт и экспорт": "가져오기/내보내기", + "Скачать JSON": "JSON 다운로드", + "Проверить файл": "파일 확인", + "Импортировать": "가져오기", + "Паспорт автомобиля": "차량 패스포트", + "Выберите автомобиль": "차량 선택", + "Параметры авто": "차량 설정", + "Сохранить паспорт": "패스포트 저장", + "Удалить автомобиль": "차량 삭제", + "Готов к работе": "준비 완료", + "Сохраняю...": "저장 중...", + "Сохранено": "저장됨", + "Ошибка": "오류", + "Нет данных": "데이터 없음", + "Нет доступа": "접근 불가", + }, + }; + + function t(text, locale = currentLocale()) { + return dictionaries[locale]?.[text] || text; + } + + function currentLocale() { + return window.CarPassPage?.state?.user?.locale || localStorage.getItem("carpassLocale") || "ru"; + } + + function apply(root = document.body, locale = currentLocale()) { + document.documentElement.lang = locale; + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + const parent = node.parentElement; + if (!parent || ["SCRIPT", "STYLE", "TEXTAREA", "INPUT", "SELECT"].includes(parent.tagName)) return NodeFilter.FILTER_REJECT; + return node.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; + }, + }); + while (walker.nextNode()) { + const node = walker.currentNode; + if (!textNodes.has(node)) textNodes.set(node, node.nodeValue.trim()); + const original = textNodes.get(node); + node.nodeValue = node.nodeValue.replace(node.nodeValue.trim(), t(original, locale)); + } + root.querySelectorAll?.("[placeholder], [aria-label], [title]").forEach((element) => { + ["placeholder", "aria-label", "title"].forEach((attr) => { + const value = element.getAttribute(attr); + if (!value) return; + let originals = attrOriginals.get(element); + if (!originals) { + originals = {}; + attrOriginals.set(element, originals); + } + originals[attr] ||= value; + element.setAttribute(attr, t(originals[attr], locale)); + }); + }); + } + + function observe(root = document.body) { + if (observer || !root) return; + observer = new MutationObserver(() => { + window.clearTimeout(timer); + timer = window.setTimeout(() => apply(root), 40); + }); + observer.observe(root, { childList: true, subtree: true }); + } + + return { apply, t, currentLocale, observe }; +})(); + const CarPassPage = (() => { const state = { user: null, authConfig: null }; @@ -51,13 +283,14 @@ const CarPassPage = (() => { } document.body.classList.remove("auth-required"); document.querySelector("#authOverlay")?.classList.add("hidden"); + localStorage.setItem("carpassLocale", state.user.locale || "ru"); return state.user; } function toast(message, tone = "success") { const node = document.querySelector("#toast"); if (!node) return; - node.textContent = message; + node.textContent = t(message); node.className = `toast ${tone}`; window.clearTimeout(toast.timer); toast.timer = window.setTimeout(() => node.classList.add("hidden"), 2600); @@ -69,7 +302,7 @@ const CarPassPage = (() => { button.dataset.label = button.textContent; button.disabled = true; button.classList.add("is-busy"); - button.innerHTML = `${label}`; + button.innerHTML = `${t(label)}`; } else { button.disabled = false; button.classList.remove("is-busy"); @@ -115,18 +348,73 @@ const CarPassPage = (() => { if (!value) return "-"; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value).slice(0, 16).replace("T", " "); - return date.toLocaleString("ru-RU", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" }); + const locale = { ru: "ru-RU", en: "en-US", ko: "ko-KR" }[state.user?.locale] || "ru-RU"; + return date.toLocaleString(locale, { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" }); } function today() { return new Date().toISOString().slice(0, 10); } + function t(text) { + return CarPassI18n.t(text, state.user?.locale || "ru"); + } + + function applyTranslations(root = document.body) { + CarPassI18n.apply(root, state.user?.locale || "ru"); + } + + async function updateLocale(locale) { + if (!state.user || state.user.locale === locale) return; + state.user = await api(`/users/${state.user.id}/preferences`, { + method: "PATCH", + body: JSON.stringify({ locale }), + }); + localStorage.setItem("carpassLocale", locale); + applyTranslations(); + } + + function installLocaleSwitch() { + const topbar = document.querySelector(".topbar"); + if (!topbar || document.querySelector("#globalLocaleSelect")) return; + let host = topbar.querySelector(".top-actions"); + if (!host) { + host = document.createElement("div"); + host.className = "top-actions"; + topbar.appendChild(host); + } + const select = document.createElement("select"); + select.id = "globalLocaleSelect"; + select.className = "locale-switch"; + select.setAttribute("aria-label", "Язык"); + select.innerHTML = ` + + + + `; + select.value = state.user?.locale || "ru"; + select.addEventListener("change", async () => { + try { + await updateLocale(select.value); + toast("Сохранено"); + } catch (error) { + toast(error.message || "Ошибка", "error"); + } + }); + const primarySelect = host.querySelector("select:not(.locale-switch)"); + if (primarySelect) primarySelect.insertAdjacentElement("afterend", select); + else host.prepend(select); + } + async function boot(init) { try { await loadAuthConfig(); await ensureUser(); + installLocaleSwitch(); + applyTranslations(); + CarPassI18n.observe(); await init(); + applyTranslations(); } catch (error) { if (error.message === "Требуется вход через Telegram") return; console.error(error); @@ -148,5 +436,9 @@ const CarPassPage = (() => { csvList, formatDateTime, today, + t, + applyTranslations, + installLocaleSwitch, + updateLocale, }; })(); diff --git a/web/static/styles.css b/web/static/styles.css index f55a8dd..fe55396 100644 --- a/web/static/styles.css +++ b/web/static/styles.css @@ -2579,7 +2579,7 @@ select { .sto-page .top-actions { display: grid; - grid-template-columns: minmax(0, 1fr) 44px; + grid-template-columns: minmax(0, 1fr) 58px 38px; } .staff-form button { @@ -2597,3 +2597,265 @@ select { padding: 20px; } } + +/* Compact UX pass */ +.locale-switch { + width: 66px; + min-height: 34px; + padding: 0 8px; + border-radius: 8px; + background: #fff; + color: var(--text); + font-size: 12px; + font-weight: 850; +} + +.shell { + padding-top: 10px; +} + +.topbar { + padding-block: 9px; +} + +.topbar h1 { + font-size: clamp(23px, 4vw, 34px); +} + +.eyebrow { + font-size: 11px; + letter-spacing: 0.03em; +} + +button, +.ghost-btn { + min-height: 38px; + padding-inline: 12px; +} + +.icon-btn { + width: 38px; + min-height: 38px; +} + +input, +select, +textarea { + min-height: 38px; + padding-inline: 10px; +} + +label { + gap: 4px; + font-size: 12px; +} + +.summary-card { + min-height: 92px; + padding: 13px; +} + +.summary-card strong { + font-size: clamp(19px, 3.2vw, 27px); +} + +.primary-add-btn { + min-height: 46px; + margin-bottom: 12px; +} + +.panel, +.workspace, +.chart-card { + padding: 13px; +} + +.passport-panel { + gap: 8px; + padding: 11px; +} + +.passport-head h2, +h2 { + font-size: 17px; +} + +.stats, +.hero-grid, +.layout, +.charts, +.flow-layout, +.sto-grid { + gap: 10px; +} + +.stat, +.stat-card { + min-height: 76px; + padding: 10px; +} + +.stat strong, +.stat-card strong { + font-size: clamp(18px, 2.1vw, 23px); +} + +.grid-form, +.entry-form, +.flow-form { + gap: 9px; +} + +.entry-form { + padding: 12px; +} + +.drawer-panel { + width: min(560px, 100%); + gap: 9px; + padding: 14px; +} + +.drawer-menu { + grid-template-columns: repeat(auto-fit, minmax(145px, 1fr)); + gap: 8px; + max-height: min(28vh, 230px); +} + +.menu-group { + padding: 8px; +} + +.menu-row { + min-height: 40px; + padding-inline: 11px; +} + +.quick-entry-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 9px; +} + +.quick-entry-grid button { + display: grid; + gap: 4px; + min-height: 82px; + align-content: center; + background: #fff; + color: var(--text); + border: 1px solid var(--line); + box-shadow: 0 8px 20px rgba(27, 38, 34, 0.06); + text-align: left; +} + +.quick-entry-grid button span { + font-size: 15px; +} + +.quick-entry-grid button small { + color: var(--muted); + font-size: 12px; + line-height: 1.25; +} + +.advanced-fields { + display: grid; + grid-column: 1 / -1; + gap: 8px; + border: 1px solid var(--line); + border-radius: 8px; + background: #fbfdfc; +} + +.advanced-fields summary { + min-height: 38px; + padding: 10px 12px; + color: var(--text); + cursor: pointer; + font-size: 12px; + font-weight: 850; + list-style: none; +} + +.advanced-fields summary::-webkit-details-marker { + display: none; +} + +.advanced-fields summary::after { + content: "+"; + float: right; + color: var(--muted); +} + +.advanced-fields[open] summary::after { + content: "-"; +} + +.compact-inner-form { + margin: 0; + padding: 0 10px 10px; + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.compact-filter { + width: 100%; + padding: 0; +} + +.compact-filter label, +.compact-filter button { + margin: 0 10px 10px; +} + +.quick-service-pills { + margin-top: -4px; +} + +.quick-service-pills button.active { + border-color: rgba(18, 115, 95, 0.45); + background: #e7f4ef; + color: #0e604f; +} + +.flow-hero { + padding: 14px; +} + +.flow-hero h2 { + font-size: clamp(20px, 2.4vw, 28px); +} + +.form-block { + padding: 10px; +} + +.admin-table th, +.admin-table td { + padding: 7px 9px; +} + +@media (max-width: 640px) { + .top-actions { + gap: 6px; + } + + .locale-switch { + width: 58px; + } + + .quick-entry-grid, + .compact-inner-form { + grid-template-columns: 1fr; + } + + .drawer-menu { + grid-template-columns: 1fr; + max-height: min(32vh, 260px); + } + + .summary-card, + .stat { + min-height: 74px; + } +}