From 2d2d74a08d4a7c2eebb9291cdb68a4bfdbf998dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 6 Sep 2026 06:57:24 +0200 Subject: [PATCH 1/3] feat(top_bar): animowane chowanie paska przy oknie wyszukiwania MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pasek `nav.sticky-header` znikał i pojawiał się skokowo przy otwieraniu i zamykaniu okna wyszukiwania globalnego. Nie był to brak animacji, tylko efekt uboczny blokady przewijania: `openGlobalSearch()` ustawia na `position: fixed` z ujemnym `top`, co łamie `position: sticky` paska — przestaje być kontenerem scrollującym, więc pasek wraca na swoją pozycję w dokumencie i na przewiniętej stronie wypada poza kadr. Przywrócenie stanu siedziało dodatkowo w `setTimeout(..., 350)`, więc pasek wracał dopiero PO zwinięciu okna, zamiast równolegle z nim. Na czas trwania modala przypinamy pasek ręcznie (`global-search-pinned` → `position: fixed`, z kompensacją `padding-top` na , żeby treść widoczna przez rozmyte tło nie podskoczyła) i dopiero wtedy wysuwamy go klasą `global-search-hidden` (`translateY(-100%)` + `opacity: 0`). Asymetria czasów jest w czystym CSS, bez dodatkowych timerów: przejście używa `transition-delay` stanu DOCELOWEGO, więc `0.1s` na regule bazowej dotyczy powrotu paska, a `0s` w `.global-search-hidden` — chowania. Pasek wraca teraz równolegle ze zwijaniem okna. Uwzględnia `prefers-reduced-motion: reduce`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01A3baC9RxFJRjzXA6yiUrF3 --- ...a-paska-nawigacji-wyszukiwanie.feature.rst | 2 + src/bpp/static/scss/top_bar.scss | 43 +++++++++++++++++++ .../templates/global_search_modal.html | 37 ++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 src/bpp/newsfragments/animacja-paska-nawigacji-wyszukiwanie.feature.rst diff --git a/src/bpp/newsfragments/animacja-paska-nawigacji-wyszukiwanie.feature.rst b/src/bpp/newsfragments/animacja-paska-nawigacji-wyszukiwanie.feature.rst new file mode 100644 index 000000000..be4d9b0fa --- /dev/null +++ b/src/bpp/newsfragments/animacja-paska-nawigacji-wyszukiwanie.feature.rst @@ -0,0 +1,2 @@ +Pasek nawigacji chowa się i wraca płynną animacją przy otwieraniu oraz +zamykaniu okna wyszukiwania globalnego, zamiast znikać i pojawiać się skokowo. diff --git a/src/bpp/static/scss/top_bar.scss b/src/bpp/static/scss/top_bar.scss index ae179ec35..af49c231b 100644 --- a/src/bpp/static/scss/top_bar.scss +++ b/src/bpp/static/scss/top_bar.scss @@ -18,6 +18,49 @@ nav.sticky-header { z-index: 2000; // Must be highest to ensure dropdown appears above all sticky elements backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); + + // Chowanie paska na czas otwartego okna wyszukiwania globalnego. + // + // Modal blokuje przewijanie ustawiajac na `position: fixed` + // z ujemnym `top` (global_search_modal.html). To lamie `position: + // sticky` powyzej: przestaje byc kontenerem scrollujacym, wiec + // pasek wraca na swoja pozycje w dokumencie i - gdy strona byla + // przewinieta - znika skokowo za gorna krawedzia. Dlatego na czas + // trwania modala przypinamy go recznie klasa `global-search-pinned` + // i dopiero wtedy mozemy go plynnie wysunac klasa + // `global-search-hidden`. Obie klasy zdejmuje `closeGlobalSearch()`. + // + // `position: fixed` jest tu wzgledem viewportu, bo zaden przodek + // (, ) nie ma transform/filter/perspective/contain - + // samo `position: relative`/`fixed` na containing blocku dla + // fixed nie tworzy. + transition: transform 0.25s ease-out, opacity 0.25s ease-out; + + // Opoznienie dotyczy stanu DOCELOWEGO przejscia, czyli powrotu paska: + // wjezdza chwile po starcie zwijania okna, zamiast scigac sie z nim. + transition-delay: 0.1s; + + &.global-search-pinned { + position: fixed; + top: 0; + left: 0; + right: 0; + } + + &.global-search-hidden { + transform: translateY(-100%); + opacity: 0; + pointer-events: none; + + // Chowanie ma ruszac natychmiast, rownolegle z rozwijaniem okna. + transition-delay: 0s; + } +} + +@media (prefers-reduced-motion: reduce) { + nav.sticky-header { + transition: none; + } } .top-bar { diff --git a/src/django_bpp/templates/global_search_modal.html b/src/django_bpp/templates/global_search_modal.html index 45aa381f1..0b6da1f82 100644 --- a/src/django_bpp/templates/global_search_modal.html +++ b/src/django_bpp/templates/global_search_modal.html @@ -479,6 +479,12 @@

Jak wyszukiwać?

// Save scroll position and block background scrolling scrollPosition = window.pageYOffset || document.documentElement.scrollTop; + // Zmierz pasek nawigacji ZANIM cokolwiek ruszymy - za chwile + // wypadnie z flow i jego wysokosc bedzie potrzebna do + // kompensacji na . + var nav = document.querySelector('nav.sticky-header'); + var navHeight = nav ? nav.offsetHeight : 0; + // Lock the body completely document.body.style.position = 'fixed'; document.body.style.top = '-' + scrollPosition + 'px'; @@ -490,6 +496,19 @@

Jak wyszukiwać?

// Also lock html element for extra security document.documentElement.style.overflow = 'hidden'; + // Przypnij pasek nawigacji do viewportu i wysun go poza kadr. + // Blokada scrolla powyzej (body -> position: fixed) lamie jego + // `position: sticky`, wiec bez tego pasek znikalby skokowo; + // klasy sa zdejmowane w closeGlobalSearch(). Reflow miedzy + // klasami jest konieczny: bez niego przegladarka scala oba + // zapisy stylu w jeden i przejscia nie ma. + if (nav) { + nav.classList.add('global-search-pinned'); + document.body.style.paddingTop = navHeight + 'px'; + void nav.offsetHeight; + nav.classList.add('global-search-hidden'); + } + // Remove previous animation classes container.classList.remove('animating-out'); @@ -622,11 +641,19 @@

Jak wyszukiwać?

window.closeGlobalSearch = function() { var modal = document.getElementById('globalSearchModal'); var container = document.querySelector('.global-search-container'); + var nav = document.querySelector('nav.sticky-header'); // Add closing animation container.classList.add('animating-out'); modal.classList.add('closing'); + // Pasek wjezdza ROWNOLEGLE ze zwijaniem okna, nie po nim. + // Wlasne opoznienie 0.1s ma w CSS (top_bar.scss), zeby ruch nie + // startowal dokladnie w tej samej klatce co collapseToButton. + if (nav) { + nav.classList.remove('global-search-hidden'); + } + // After animation completes, hide modal and restore scrolling setTimeout(function() { modal.classList.remove('show', 'closing'); @@ -647,6 +674,16 @@

Jak wyszukiwać?

// Restore scroll position window.scrollTo(0, scrollPosition); + // Oddaj pasek z powrotem `position: sticky`. Musi to isc w + // tej samej klatce co przywrocenie scrolla powyzej: pasek + // przypiety (fixed, top 0) i sticky przy top 0 sa w tym + // samym miejscu, wiec przelaczenie jest niewidoczne, a + // kompensacja paddingu znika razem z powrotem nav do flow. + if (nav) { + nav.classList.remove('global-search-pinned'); + document.body.style.paddingTop = ''; + } + // Reset container styles for next opening container.style.transform = ''; container.style.opacity = ''; From 697a08d98fac5679aa1538ec26aa4a028081a47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Thu, 10 Sep 2026 19:03:27 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(global=5Fsearch):=20szk=C5=82o=20narast?= =?UTF-8?q?a=20nad=20nieruchom=C4=85=20stron=C4=85=20zamiast=20wysuwa?= =?UTF-8?q?=C4=87=20pasek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - blokada przewijania przez `overflow: hidden` na zamiast `position: fixed` na , które łamało `position: sticky` paska i breadcrumbs (biała szczelina i skok po ESC), - szkło pojawia się i gaśnie przez `opacity` + `visibility`; od `display: none` przejście CSS nigdy nie startowało, - okno i szkło mają wspólny czas 0.35s, bez setTimeout w JS, - usunięty zdublowany CSS animacji z szablonu (wygrywał z SCSS), - testy Playwright: układ strony w każdej klatce animacji oraz płynne krycie szkła; zweryfikowane na chromium, firefox i webkit. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Tu4mhWdY4zbQ82N74krLqZ --- ...a-paska-nawigacji-wyszukiwanie.feature.rst | 2 - ...-wyszukiwania-bez-skokow-ukladu.bugfix.rst | 3 + src/bpp/static/scss/top_bar.scss | 112 +++++----- .../templates/global_search_modal.html | 208 +++--------------- src/integration_tests/test_global_search.py | 167 ++++++++++++++ 5 files changed, 251 insertions(+), 241 deletions(-) delete mode 100644 src/bpp/newsfragments/animacja-paska-nawigacji-wyszukiwanie.feature.rst create mode 100644 src/bpp/newsfragments/okno-wyszukiwania-bez-skokow-ukladu.bugfix.rst diff --git a/src/bpp/newsfragments/animacja-paska-nawigacji-wyszukiwanie.feature.rst b/src/bpp/newsfragments/animacja-paska-nawigacji-wyszukiwanie.feature.rst deleted file mode 100644 index be4d9b0fa..000000000 --- a/src/bpp/newsfragments/animacja-paska-nawigacji-wyszukiwanie.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Pasek nawigacji chowa się i wraca płynną animacją przy otwieraniu oraz -zamykaniu okna wyszukiwania globalnego, zamiast znikać i pojawiać się skokowo. diff --git a/src/bpp/newsfragments/okno-wyszukiwania-bez-skokow-ukladu.bugfix.rst b/src/bpp/newsfragments/okno-wyszukiwania-bez-skokow-ukladu.bugfix.rst new file mode 100644 index 000000000..47e7bdddb --- /dev/null +++ b/src/bpp/newsfragments/okno-wyszukiwania-bez-skokow-ukladu.bugfix.rst @@ -0,0 +1,3 @@ +Otwieranie i zamykanie okna wyszukiwania globalnego nie przesuwa już paska +nawigacji ani okruszków — rozmyte tło płynnie narasta i gaśnie nad nieruchomą +stroną, bez białej szczeliny pod paskiem po naciśnięciu ESC. diff --git a/src/bpp/static/scss/top_bar.scss b/src/bpp/static/scss/top_bar.scss index af49c231b..63a8efa82 100644 --- a/src/bpp/static/scss/top_bar.scss +++ b/src/bpp/static/scss/top_bar.scss @@ -18,49 +18,6 @@ nav.sticky-header { z-index: 2000; // Must be highest to ensure dropdown appears above all sticky elements backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); - - // Chowanie paska na czas otwartego okna wyszukiwania globalnego. - // - // Modal blokuje przewijanie ustawiajac na `position: fixed` - // z ujemnym `top` (global_search_modal.html). To lamie `position: - // sticky` powyzej: przestaje byc kontenerem scrollujacym, wiec - // pasek wraca na swoja pozycje w dokumencie i - gdy strona byla - // przewinieta - znika skokowo za gorna krawedzia. Dlatego na czas - // trwania modala przypinamy go recznie klasa `global-search-pinned` - // i dopiero wtedy mozemy go plynnie wysunac klasa - // `global-search-hidden`. Obie klasy zdejmuje `closeGlobalSearch()`. - // - // `position: fixed` jest tu wzgledem viewportu, bo zaden przodek - // (, ) nie ma transform/filter/perspective/contain - - // samo `position: relative`/`fixed` na containing blocku dla - // fixed nie tworzy. - transition: transform 0.25s ease-out, opacity 0.25s ease-out; - - // Opoznienie dotyczy stanu DOCELOWEGO przejscia, czyli powrotu paska: - // wjezdza chwile po starcie zwijania okna, zamiast scigac sie z nim. - transition-delay: 0.1s; - - &.global-search-pinned { - position: fixed; - top: 0; - left: 0; - right: 0; - } - - &.global-search-hidden { - transform: translateY(-100%); - opacity: 0; - pointer-events: none; - - // Chowanie ma ruszac natychmiast, rownolegle z rozwijaniem okna. - transition-delay: 0s; - } -} - -@media (prefers-reduced-motion: reduce) { - nav.sticky-header { - transition: none; - } } .top-bar { @@ -501,8 +458,29 @@ nav.sticky-header { } // Global search modal styles +// +// Okno wyszukiwania NIE może ruszać układu strony pod spodem: +// - przewijanie blokuje `overflow: hidden` na (klasa +// `global-search-open`), a nie `position: fixed` na . Ta druga +// technika łamie `position: sticky` paska nawigacji i breadcrumbs, +// które wtedy przeskakują; +// - szkło pojawia się i gaśnie przez `opacity` + `visibility`, a nie +// przez `display: none`, bo od `display: none` przejście CSS nigdy +// nie startuje (szkło wskakiwało w jednej klatce). +// Pilnują tego testy w src/integration_tests/test_global_search.py. +$global-search-duration: 0.35s; + +html.global-search-open { + overflow: hidden; +} + +// Ustawiana tylko przy klasycznym pasku przewijania (o niezerowej +// szerokości): rezerwuje jego miejsce, żeby treść nie drgnęła w poziomie. +html.global-search-scrollbar-gutter { + scrollbar-gutter: stable; +} + .global-search-modal { - display: none; position: fixed; top: 0; left: 0; @@ -512,25 +490,18 @@ nav.sticky-header { backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); z-index: 10000; + visibility: hidden; opacity: 0; - transition: opacity 0.3s ease; + // `visibility` przełącza się skokowo, ale respektuje opóźnienie: + // przy chowaniu gaśnie dopiero po wygaszeniu krycia. + transition: opacity $global-search-duration ease, + visibility 0s linear $global-search-duration; &.show { - display: block; - opacity: 1; - } - - &.closing { - animation: fadeOutModal 0.4s ease-out forwards; - } -} - -@keyframes fadeOutModal { - from { + visibility: visible; opacity: 1; - } - to { - opacity: 0; + transition: opacity $global-search-duration ease, + visibility 0s linear 0s; } } @@ -547,8 +518,11 @@ nav.sticky-header { transform: scale(0); pointer-events: none; + // Ten sam czas co szkło: okno rusza w tej samej klatce, w której + // szkło zaczyna narastać/gasnąć, i kończy razem z nim. &.animating-out { - animation: collapseToButton 0.4s cubic-bezier(0.64, 0, 0.66, 0.44) forwards !important; + animation: collapseToButton $global-search-duration + cubic-bezier(0.4, 0, 1, 1) forwards !important; pointer-events: none !important; } } @@ -557,7 +531,8 @@ nav.sticky-header { pointer-events: auto; &:not(.animating-out) { - animation: expandFromButton 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; + animation: expandFromButton $global-search-duration + cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } } @@ -581,8 +556,8 @@ nav.sticky-header { transform: scale(1); opacity: 1; } - 40% { - transform: scale(1.05); + 30% { + transform: scale(0.95); opacity: 1; } to { @@ -591,6 +566,17 @@ nav.sticky-header { } } +@media (prefers-reduced-motion: reduce) { + // Bez skalowania: okno narasta i gaśnie po prostu razem ze szkłem. + .global-search-container, + .global-search-container.animating-out, + .global-search-modal.show .global-search-container:not(.animating-out) { + animation: none !important; + opacity: 1; + transform: none; + } +} + .global-search-input { width: 100%; font-size: 1.2rem; diff --git a/src/django_bpp/templates/global_search_modal.html b/src/django_bpp/templates/global_search_modal.html index 0b6da1f82..8714be4ac 100644 --- a/src/django_bpp/templates/global_search_modal.html +++ b/src/django_bpp/templates/global_search_modal.html @@ -1,96 +1,6 @@