Skip to content

feat(top_bar): animowane chowanie paska przy oknie wyszukiwania - #800

Open
mpasternak wants to merge 1 commit into
devfrom
worktree-topbar-animacja
Open

feat(top_bar): animowane chowanie paska przy oknie wyszukiwania#800
mpasternak wants to merge 1 commit into
devfrom
worktree-topbar-animacja

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Problem

Po naciśnięciu / pasek nav.sticky-header znikał skokowo, a po ESC wracał dopiero po zwinięciu okna wyszukiwania. Wyglądało to na brak animacji, ale przyczyna jest inna.

openGlobalSearch() blokuje przewijanie klasycznym scroll-lockiem:

document.body.style.position = 'fixed';
document.body.style.top = '-' + scrollPosition + 'px';

To łamie position: sticky paska (top_bar.scss): <body> przestaje być kontenerem scrollującym, więc pasek wraca na swoją pozycję w dokumencie — czyli poza górną krawędź, jeśli strona była przewinięta. Przy scrollu 0 błąd był niewidoczny, stąd łatwo go było przeoczyć.

Przywrócenie stanu siedziało dodatkowo w setTimeout(..., 350), czyli wykonywało się po zakończeniu collapseToButton — stąd sekwencyjność przy zamykaniu.

Rozwiązanie

Na czas trwania modala pasek przechodzi pod ręczną kontrolę:

  • .global-search-pinnedposition: fixed; top: 0 (zastępuje niedziałające sticky), z kompensacją padding-top na <body>, żeby treść widoczna przez rozmyte tło nie podskoczyła o wysokość paska;
  • .global-search-hiddentranslateY(-100%) + opacity: 0, przejście 250 ms ease-out.

Przy zamykaniu klasa chowająca znika na początku closeGlobalSearch(), więc pasek wjeżdża równolegle ze zwijaniem okna. Odpięcie idzie w tej samej klatce co window.scrollTo — pasek przypięty (fixed, top: 0) i sticky przy top: 0 są w tym samym miejscu, więc przełączenie jest niewidoczne.

Asymetria czasów bez timerów

Chowanie ma ruszać natychmiast, powrót — z lekkim opóźnieniem. Załatwione czystym CSS, bo przejście używa transition-* stanu docelowego:

nav.sticky-header {
    transition: transform .25s ease-out, opacity .25s ease-out;
    transition-delay: .1s;        // cel = widoczny  → POWRÓT
    &.global-search-hidden {
        transition-delay: 0s;     // cel = ukryty    → CHOWANIE
    }
}

Dlaczego void nav.offsetHeight

nav.classList.add('global-search-pinned');
void nav.offsetHeight;            // wymuszony reflow
nav.classList.add('global-search-hidden');

Bez tego obie klasy trafiłyby do jednego przeliczenia stylu, element nigdy nie miałby stanu początkowego i pasek znikałby skokowo — dokładnie tak jak przed zmianą. Linijka wygląda na martwy zapis, więc ma w kodzie komentarz wyjaśniający.

Uwzględnione prefers-reduced-motion: reduce.

Uwaga o position: fixed

<nav> jest bezpośrednim dzieckiem <body> (base.html:229), a body ma position: relative przez @extend .gradient-background. To bezpieczne — containing block dla fixed tworzą tylko transform, filter, perspective, contain i will-change, nie position. Przypięty pasek pozostaje względem viewportu także wtedy, gdy scroll-lock ustawi body { position: fixed }.

Testy

src/integration_tests/test_global_search.py3 passed (strona renderuje się bez błędów, / otwiera okno, wyniki się ładują, ENTER nawiguje).

Sam ruch animacji nie jest pokryty testem — świadoma decyzja, weryfikacja wizualna jest ręczna.

Jak sprawdzić ręcznie

uv run run-site run --from-dump ~/db-backup-20260428-093811.pg_dump

Przewiń stronę w dół przed naciśnięciem / — przy scrollu 0 różnicy nie widać.

Warto zerknąć na dwie rzeczy: czy treść pod rozmytym tłem nie drga przy otwieraniu (kompensacja padding-top) i czy nie ma przeskoku w chwili oddania paska z powrotem pod sticky przy zamykaniu.

Strojenie to dwie liczby w top_bar.scss: 0.25s (czas) i 0.1s (opóźnienie powrotu).

🤖 Generated with Claude Code

https://claude.ai/code/session_01A3baC9RxFJRjzXA6yiUrF3

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 <body>
`position: fixed` z ujemnym `top`, co łamie `position: sticky` paska —
<body> 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 <body>, ż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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A3baC9RxFJRjzXA6yiUrF3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant