diff --git a/docs/astrololo_wymagania.xlsx b/docs/astrololo_wymagania.xlsx index c7bb6d5..7935e61 100644 Binary files a/docs/astrololo_wymagania.xlsx and b/docs/astrololo_wymagania.xlsx differ diff --git a/services/presentation/app/static/styles.css b/services/presentation/app/static/styles.css index c9c7d94..85ad0f0 100644 --- a/services/presentation/app/static/styles.css +++ b/services/presentation/app/static/styles.css @@ -50,6 +50,24 @@ td.glyph { color: var(--accent); white-space: nowrap; } .wheel-fig figcaption { margin-top: .4rem; } /* glify wewnątrz SVG dziedziczą monochromatyczny stack fontów (DAN-18) */ svg.wheel text.glyph { font-size: inherit; } + +/* Powiększenie na pełne okno (PRE-25). Kliknięcie rozciąga wykres na cały ekran, + ponowne wraca. z-index 1100 CELOWO pomiędzy: ponad kontrolki Leafleta (1000), + ale PONIŻEJ okna postępu (1200) — gdy trwa pisanie horoskopu, log operacji ma + zostać na wierzchu. */ +.wheel-fig svg.wheel { cursor: zoom-in; } +.wheel-fig svg.wheel:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; } +.wheel-fig.zoomed { + position: fixed; inset: 0; z-index: 1100; margin: 0; + display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: .5rem; padding: 1rem; background: var(--bg); +} +/* min() po obu wymiarach: koło jest kwadratowe, więc ograniczamy je krótszym + bokiem okna — inaczej na szerokim ekranie wystawałoby w pionie. */ +.wheel-fig.zoomed svg.wheel { + max-width: min(96vw, 92vh); width: min(96vw, 92vh); height: auto; cursor: zoom-out; +} +body.wheel-zoom-open { overflow: hidden; } /* nie przewijaj treści pod spodem */ form { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem; } .row { display: flex; gap: .5rem; } .row input[type=text] { flex: 1; } diff --git a/services/presentation/app/static/wheelzoom.js b/services/presentation/app/static/wheelzoom.js new file mode 100644 index 0000000..22a3e26 --- /dev/null +++ b/services/presentation/app/static/wheelzoom.js @@ -0,0 +1,60 @@ +// Powiększanie kosmogramu na pełne okno (PRE-25). +// +// Koło rysujemy w kolumnie tekstu, więc na małym ekranie glify i stopnie robią +// się nieczytelne. Kliknięcie rozciąga wykres na całe okno, ponowne wraca do +// strony. SVG skaluje się bez utraty jakości, więc nie potrzeba drugiej wersji +// rysunku ani żadnej biblioteki. +// +// Podpięte globalnie i osłonięte sprawdzeniem, czy koło w ogóle jest na stronie +// — dzięki temu zadziała też na przyszłej zakładce „Skompiluj", bez zmian tutaj. +(function () { + 'use strict'; + + var ZOOMED = 'zoomed'; + + function ready(fn) { + if (document.readyState !== 'loading') fn(); + else document.addEventListener('DOMContentLoaded', fn); + } + + ready(function () { + var figures = document.querySelectorAll('.wheel-fig'); + if (!figures.length) return; + + Array.prototype.forEach.call(figures, function (fig) { + var svg = fig.querySelector('svg.wheel'); + if (!svg) return; + + // Klawiatura ma działać tak samo jak mysz — to nie jest ozdobnik, tylko + // jedyny sposób na obejrzenie szczegółów wykresu. + svg.setAttribute('tabindex', '0'); + svg.setAttribute('role', 'button'); + svg.setAttribute('aria-label', 'Kosmogram — kliknij, aby powiększyć na całe okno'); + + function toggle() { + var on = fig.classList.toggle(ZOOMED); + // Blokujemy przewijanie strony pod spodem: inaczej scroll „przecieka" + // na treść, której i tak nie widać. + document.body.classList.toggle('wheel-zoom-open', on); + svg.setAttribute('aria-label', on + ? 'Kosmogram powiększony — kliknij lub naciśnij Esc, aby wrócić' + : 'Kosmogram — kliknij, aby powiększyć na całe okno'); + if (on) svg.focus(); + } + + svg.addEventListener('click', toggle); + svg.addEventListener('keydown', function (e) { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } + }); + + // Klik w tło (poza samym rysunkiem) też zamyka — naturalne dla nakładki. + fig.addEventListener('click', function (e) { + if (e.target === fig && fig.classList.contains(ZOOMED)) toggle(); + }); + + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && fig.classList.contains(ZOOMED)) toggle(); + }); + }); + }); +})(); diff --git a/services/presentation/app/templates/base.html b/services/presentation/app/templates/base.html index fce2746..3bfb405 100644 --- a/services/presentation/app/templates/base.html +++ b/services/presentation/app/templates/base.html @@ -10,6 +10,9 @@ odtworzyć współrzędne, ZANIM geo.js zbuduje mapę — mapa startuje od razu we właściwym miejscu, bez przestawiania. #} + {# Powiększanie kosmogramu (PRE-25) — globalnie, bo koło pojawi się też na + zakładce „Skompiluj"; skrypt sam sprawdza, czy jest co powiększać. #} +
diff --git a/services/presentation/tests/test_wheelzoom.py b/services/presentation/tests/test_wheelzoom.py new file mode 100644 index 0000000..66faf8b --- /dev/null +++ b/services/presentation/tests/test_wheelzoom.py @@ -0,0 +1,64 @@ +"""Powiększanie kosmogramu na pełne okno (PRE-25). + +Test strukturalny, jak przy pozostałych rzeczach żyjących w JS/CSS. Osobno +pilnujemy warstwy nakładki: to jedyna liczba w tym pliku, którą łatwo zmienić +bez zastanowienia i zepsuć coś, czego się nie widzi na pierwszy rzut oka. +""" +import pathlib +import re + +APP = pathlib.Path(__file__).resolve().parents[1] / "app" +JS = (APP / "static" / "wheelzoom.js").read_text(encoding="utf-8") +CSS = (APP / "static" / "styles.css").read_text(encoding="utf-8") +BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8") + + +def test_script_is_loaded_globally(): + """Ładowane w base.html, bo koło pojawi się też na zakładce Skompiluj.""" + assert "wheelzoom.js" in BASE + + +def test_toggles_on_click(): + assert "addEventListener('click'" in JS + assert "classList.toggle" in JS + + +def test_closes_with_escape(): + assert "'Escape'" in JS + + +def test_reachable_from_keyboard(): + """Bez tego jedyny sposób na obejrzenie szczegółów wykresu wymagałby myszy.""" + assert "tabindex" in JS + assert "'Enter'" in JS + + +def test_page_scroll_is_locked_while_zoomed(): + """Inaczej przewijanie „przecieka" na treść pod spodem, której i tak nie widać.""" + assert "wheel-zoom-open" in JS + assert re.search(r"body\.wheel-zoom-open\s*\{[^}]*overflow:\s*hidden", CSS) + + +def test_zoomed_layer_sits_above_map_but_below_progress(): + """Warstwy: kontrolki Leafleta 1000 < kosmogram < okno postępu 1200. + Gdy trwa pisanie horoskopu, log operacji ma zostać na wierzchu.""" + zoom = re.search(r"\.wheel-fig\.zoomed\s*\{[^}]*z-index:\s*(\d+)", CSS) + progress = re.search(r"\.progress-overlay\s*\{[^}]*z-index:\s*(\d+)", CSS) + assert zoom and progress, "nie znaleziono warstw w CSS" + assert 1000 < int(zoom.group(1)) < int(progress.group(1)) + + +def test_zoomed_wheel_is_bounded_by_shorter_window_side(): + """Koło jest kwadratowe — ograniczamy je krótszym bokiem okna, inaczej na + szerokim ekranie wystawałoby w pionie.""" + block = re.search(r"\.wheel-fig\.zoomed svg\.wheel\s*\{[^}]*\}", CSS) + assert block and "vh" in block.group(0) and "vw" in block.group(0) + + +def test_cursor_signals_the_affordance(): + assert "zoom-in" in CSS and "zoom-out" in CSS + + +def test_does_nothing_without_a_wheel(): + """Na zakładkach bez kosmogramu skrypt ma po prostu wyjść.""" + assert "if (!figures.length) return;" in JS