From 54446d7b0f0abc450ccadf93b815e381cb816642 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Mon, 2 Mar 2026 20:21:33 +0300 Subject: [PATCH] feat: enhance UI components and error handling - Updated HTML structure for navigation buttons in the calendar, adding SVG icons for improved visual clarity. - Introduced a new muted text style in CSS for better presentation of empty duty list messages. - Enhanced calendar CSS for navigation buttons and day indicators, improving layout and responsiveness. - Improved error handling in the UI by adding retry functionality to the error display, allowing users to retry actions directly from the error message. - Updated internationalization messages to include a retry option for error handling. - Added unit tests to verify the new error handling behavior and UI updates. --- webapp/css/base.css | 9 +++++++ webapp/css/calendar.css | 35 ++++++++++++++++++++++-- webapp/css/day-detail.css | 12 ++++++--- webapp/css/duty-list.css | 10 +++++-- webapp/css/hints.css | 2 +- webapp/css/states.css | 57 ++++++++++++++++++++++++++++++++++++--- webapp/index.html | 9 ++++--- webapp/js/dayDetail.js | 6 ++++- webapp/js/i18n.js | 2 ++ webapp/js/main.js | 2 +- webapp/js/ui.js | 26 ++++++++++++++++-- webapp/js/ui.test.js | 3 ++- 12 files changed, 154 insertions(+), 19 deletions(-) diff --git a/webapp/css/base.css b/webapp/css/base.css index 6eea6c4..3c8c7af 100644 --- a/webapp/css/base.css +++ b/webapp/css/base.css @@ -89,6 +89,15 @@ body { padding-bottom: env(safe-area-inset-bottom, 12px); } +/* Muted text (e.g. empty duty list message). */ +.muted { + color: var(--muted); +} + [data-theme="light"] .container { border-radius: 12px; } + +[data-theme="dark"] .container { + border-radius: 12px; +} diff --git a/webapp/css/calendar.css b/webapp/css/calendar.css index 74908a2..f6036d0 100644 --- a/webapp/css/calendar.css +++ b/webapp/css/calendar.css @@ -21,7 +21,21 @@ font-size: 24px; line-height: 1; cursor: pointer; - transition: opacity var(--transition-fast), transform var(--transition-fast); + display: inline-flex; + align-items: center; + justify-content: center; + transition: opacity var(--transition-fast), transform var(--transition-fast), + background-color var(--transition-fast); +} + +.nav svg { + display: block; +} + +@media (hover: hover) { + .nav:hover { + background: color-mix(in srgb, var(--accent) 15%, var(--surface)); + } } .nav:focus { @@ -48,6 +62,12 @@ font-weight: 600; } +@media (max-width: 480px) { + .title { + font-size: 1.25rem; + } +} + .weekdays { display: grid; grid-template-columns: repeat(7, 1fr); @@ -92,13 +112,23 @@ font-size: 0.85rem; background: var(--surface); min-width: 0; - min-height: 0; + min-height: 32px; overflow: hidden; transition: background-color var(--transition-fast), transform var(--transition-fast); } +@media (hover: hover) { + .day:hover { + background: color-mix(in srgb, var(--accent) 10%, var(--surface)); + } + .day.today:hover { + background: color-mix(in srgb, var(--bg) 15%, var(--today)); + } +} + .day.other-month { opacity: 0.4; + background: color-mix(in srgb, var(--muted) 8%, var(--surface)); } .day.today { @@ -142,6 +172,7 @@ .day-indicator { display: flex; justify-content: center; + gap: 2px; width: 65%; margin-top: 6px; } diff --git a/webapp/css/day-detail.css b/webapp/css/day-detail.css index 3547aca..729972a 100644 --- a/webapp/css/day-detail.css +++ b/webapp/css/day-detail.css @@ -62,10 +62,10 @@ body.day-detail-sheet-open { .day-detail-panel--sheet::before { content: ""; display: block; - width: 36px; + width: 40px; height: 4px; margin: 0 auto 8px; - background: var(--muted); + background: color-mix(in srgb, var(--muted) 80%, var(--text)); border-radius: 2px; } @@ -79,13 +79,19 @@ body.day-detail-sheet-open { border: none; background: transparent; color: var(--muted); - font-size: 1.5rem; line-height: 1; cursor: pointer; border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; transition: opacity var(--transition-fast), background-color var(--transition-fast); } +.day-detail-close svg { + display: block; +} + .day-detail-close:focus { outline: none; } diff --git a/webapp/css/duty-list.css b/webapp/css/duty-list.css index e679bfe..13ad787 100644 --- a/webapp/css/duty-list.css +++ b/webapp/css/duty-list.css @@ -41,7 +41,12 @@ top: 0; bottom: 0; width: 2px; - background: var(--muted); + background: linear-gradient( + to bottom, + var(--muted) 0%, + var(--muted) 85%, + color-mix(in srgb, var(--muted) 40%, transparent) 100% + ); pointer-events: none; } @@ -185,7 +190,7 @@ } .duty-flip-inner { - transition: transform 0.4s; + transition: transform 0.3s; transform-style: preserve-3d; position: relative; min-height: 0; @@ -253,6 +258,7 @@ border-radius: 8px; background: var(--surface); border-left: 3px solid var(--duty); + box-shadow: 0 1px 3px color-mix(in srgb, var(--text) 8%, transparent); } .duty-item--unavailable { diff --git a/webapp/css/hints.css b/webapp/css/hints.css index c1ff8a7..54667ab 100644 --- a/webapp/css/hints.css +++ b/webapp/css/hints.css @@ -3,7 +3,7 @@ position: fixed; z-index: 1000; width: max-content; - max-width: min(98vw, 900px); + max-width: min(98vw, 380px); min-width: 0; padding: 8px 12px; background: var(--surface); diff --git a/webapp/css/states.css b/webapp/css/states.css index 0ed0105..38e4a28 100644 --- a/webapp/css/states.css +++ b/webapp/css/states.css @@ -33,7 +33,7 @@ } } -.loading, .error { +.loading { text-align: center; padding: 12px; color: var(--muted); @@ -45,7 +45,16 @@ } .error { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 20px 16px; + margin: 12px 0; + background: var(--surface); + border-radius: 12px; color: var(--error); + text-align: center; } .error[hidden], .loading.hidden, @@ -53,6 +62,36 @@ display: none !important; } +.error-icon { + flex-shrink: 0; + color: var(--error); +} + +.error-text { + margin: 0; +} + +.error-retry { + margin-top: 4px; + padding: 8px 16px; + font-size: 0.9rem; + font-weight: 500; + color: var(--bg); + background: var(--accent); + border: none; + border-radius: 8px; + cursor: pointer; +} + +.error-retry:hover { + opacity: 0.9; +} + +.error-retry:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + /* Current duty view (Mini App deep link startapp=duty) */ [data-view="currentDuty"] .calendar-sticky, [data-view="currentDuty"] .duty-list { @@ -167,11 +206,19 @@ color: var(--muted); margin-bottom: 16px; line-height: 0; + width: 48px; + height: 48px; + border-radius: 50%; + background: color-mix(in srgb, var(--muted) 25%, transparent); + padding: 12px; + box-sizing: border-box; } .current-duty-no-duty-icon svg { display: block; - opacity: 0.7; + width: 100%; + height: 100%; + opacity: 0.85; } .current-duty-no-duty { @@ -282,8 +329,12 @@ .access-denied { text-align: center; - padding: 24px 12px; + padding: 24px 16px; + margin: 12px 0; + background: var(--surface); + border-radius: 12px; color: var(--muted); + box-shadow: 0 1px 3px color-mix(in srgb, var(--text) 8%, transparent); } .access-denied p { diff --git a/webapp/index.html b/webapp/index.html index d890b32..addfab1 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -3,7 +3,6 @@ - @@ -17,9 +16,13 @@
- +

- +
diff --git a/webapp/js/dayDetail.js b/webapp/js/dayDetail.js index d0c61ec..d090add 100644 --- a/webapp/js/dayDetail.js +++ b/webapp/js/dayDetail.js @@ -339,10 +339,14 @@ function ensurePanelInDom() { panelEl.hidden = true; const closeLabel = t(state.lang, "day_detail.close"); + const closeIcon = + ''; panelEl.innerHTML = '
'; + '">' + + closeIcon + + '
'; const closeBtn = panelEl.querySelector(".day-detail-close"); if (closeBtn) { diff --git a/webapp/js/i18n.js b/webapp/js/i18n.js index 7f245ed..6ceb7e0 100644 --- a/webapp/js/i18n.js +++ b/webapp/js/i18n.js @@ -13,6 +13,7 @@ export const MESSAGES = { error_load_failed: "Load failed", error_network: "Could not load data. Check your connection.", error_generic: "Could not load data.", + "error.retry": "Retry", "nav.prev_month": "Previous month", "nav.next_month": "Next month", "weekdays.mon": "Mon", @@ -68,6 +69,7 @@ export const MESSAGES = { error_load_failed: "Ошибка загрузки", error_network: "Не удалось загрузить данные. Проверьте интернет.", error_generic: "Не удалось загрузить данные.", + "error.retry": "Повторить", "nav.prev_month": "Предыдущий месяц", "nav.next_month": "Следующий месяц", "weekdays.mon": "Пн", diff --git a/webapp/js/main.js b/webapp/js/main.js index d7c068a..c90587d 100644 --- a/webapp/js/main.js +++ b/webapp/js/main.js @@ -189,7 +189,7 @@ async function loadMonth() { } return; } - showError(e.message || t(state.lang, "error_generic")); + showError(e.message || t(state.lang, "error_generic"), loadMonth); setNavEnabled(true); return; } diff --git a/webapp/js/ui.js b/webapp/js/ui.js index 9520f25..d5c4371 100644 --- a/webapp/js/ui.js +++ b/webapp/js/ui.js @@ -15,6 +15,7 @@ import { getNextBtn } from "./dom.js"; import { t } from "./i18n.js"; +import { escapeHtml } from "./utils.js"; /** * Show access-denied view and hide calendar/list/loading/error. @@ -63,16 +64,37 @@ export function hideAccessDenied() { if (dutyListEl) dutyListEl.hidden = false; } +/** Warning icon SVG for error state (24×24). */ +const ERROR_ICON_SVG = + ''; + /** * Show error message and hide loading. * @param {string} msg - Error text + * @param {(() => void)|null} [onRetry] - Optional callback for Retry button */ -export function showError(msg) { +export function showError(msg, onRetry) { const errorEl = getErrorEl(); const loadingEl = getLoadingEl(); if (errorEl) { - errorEl.textContent = msg; + const retryLabel = t(state.lang, "error.retry"); + const safeMsg = typeof msg === "string" ? msg : t(state.lang, "error_generic"); + let html = + ERROR_ICON_SVG + + '

' + + escapeHtml(safeMsg) + + "

"; + if (typeof onRetry === "function") { + html += '"; + } + errorEl.innerHTML = html; errorEl.hidden = false; + const retryBtn = errorEl.querySelector(".error-retry"); + if (retryBtn && typeof onRetry === "function") { + retryBtn.addEventListener("click", () => { + onRetry(); + }); + } } if (loadingEl) loadingEl.classList.add("hidden"); } diff --git a/webapp/js/ui.test.js b/webapp/js/ui.test.js index 8cb0a1c..5b5d2ce 100644 --- a/webapp/js/ui.test.js +++ b/webapp/js/ui.test.js @@ -93,7 +93,8 @@ describe("ui", () => { it("sets error text and shows error element", () => { showError("Network error"); const errorEl = document.getElementById("error"); - expect(errorEl?.textContent).toBe("Network error"); + const textEl = errorEl?.querySelector(".error-text"); + expect(textEl?.textContent).toBe("Network error"); expect(errorEl?.hidden).toBe(false); });