Skip to content

feat(web): add scroll-to-top button - #201

Merged
todorkolev merged 6 commits into
midt-bg:mainfrom
StanislavBG:feat/scroll-to-top
Jul 15, 2026
Merged

feat(web): add scroll-to-top button#201
todorkolev merged 6 commits into
midt-bg:mainfrom
StanislavBG:feat/scroll-to-top

Conversation

@StanislavBG

@StanislavBG StanislavBG commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What changed

Scroll-to-top button, moved to bottom-left (to avoid overlapping the third-party accessibility launcher), with rAF-throttled scroll handling. Review fixes: respects prefers-reduced-motion (both the JS scroll behavior and CSS transitions), extracted the 400px threshold into a named SHOW_AFTER_PX constant, and preserves the raised drop-shadow under :focus-visible.

How it was tested

  • pnpm --filter web test — new coverage for visibility toggling, scrollTo invocation, and reduced-motion behavior.
  • Manual exercise with prefers-reduced-motion: reduce emulated in devtools.

Quality checks

  • CI green. Document/window-level scroll assumption confirmed and documented in-code.

@StanislavBG

Copy link
Copy Markdown
Contributor Author

@todorkolev готов за ревю 🙏 — rebase-нат на main, CI зелен, prettier-чист, CSS промените в styles/* (app.css само @import). Резолвнати нишки. Approve-ни когато ти е удобно.

@ydimitrof

Copy link
Copy Markdown
Contributor

Одобрявам на същество. Прегледах дифа изцяло, сравних с issue #176, и проверих локално за SQL/инжекционни и други уязвимости.

Обхват на прегледа

PR #201 — rebase на #176 (@Hard-system) върху актуалния main: добавя бутон „Към началото" в основния лейаут. Три файла, +113/−0: нов ScrollToTop.tsx, две реда в root.tsx, и 58 реда стилове в styles/chrome.css. Малка, фокусирана и атомарна промяна — един логически concern.

Съответствие с issue #176

Имплементацията покрива описанието на #176 точно: нов компонент с поява при скрол >400px, глобална интеграция в root.tsx, дизайн токъни (var(--ink), var(--accent), var(--paper)) за визуална консистентност, и достъпност през ARIA + клавиатурен фокус мениджмънт. Обосновката за разликите спрямо оригинала (CSS в styles/* заради split #84, поправен prettier lint) е коректна — потвърдих, че --ink/--paper/--accent са дефинирани в tokens.css и че няма дублиращ scroll-to-top компонент другаде.

Сигурност / OWASP / целостност на данните

  • Чисто. Компонентът е изцяло клиентски: няма потребителски вход, няма dangerouslySetInnerHTML, няма fetch/eval/динамичен код, няма външни URL-и, няма тайни. Не се докосват БД, заявки или данни — рискът за целостта на данните е нулев (N/A).
  • Без SQL повърхност — нищо за инжектиране; SVG-то е статичен инлайн markup, не user-controlled → без XSS вектор.
  • CSP-безопасно — няма инлайн скриптове/стилове, така че nonce политиката на root.tsx не се засяга.
  • Не открих обфускация, backdoor-и, злонамерени патърни или подозрителни зависимости (нови импорти няма — само react).

Качество и конвенции

  • resource-cleanup спазен: removeEventListener в cleanup на useEffect; listener-ът е { passive: true }.
  • SSR-безопасно: достъпът до window е само вътре в useEffect (клиент).
  • Достъпността е направена издържано — aria-hidden={!isVisible} е синхронизиран с tabIndex={isVisible ? 0 : -1}, така че скритият бутон не е нито фокусируем, нито обявяван; focus-visible стиловете и по-големият tap target на мобилно (44px) са налице.
  • Липсата на тест за компонента е в съответствие с текущата конвенция на репото (няма *.test.tsx за компоненти като SiteHeader/AccessibilityWidget), затова не го третирам като блокер.

Незадължителни бележки (не блокират)

  1. prefers-reduced-motion не се зачита — нито behavior: 'smooth', нито CSS транзишъните. Репото не използва тази media query никъде, така че е консистентно, но предвид силния accessibility фокус на платформата си струва follow-up.
  2. Възможно визуално припокриване долу-вдясно с вградената приставка за достъпност / друга fixed chrome — препоръчвам бърза визуална проверка, че двата елемента не се застъпват на реален екран.

Нищо от горните не е пречка за merge.

Verdict: Approve на същество — сигурност, OWASP и целостта на данните са чисти; кодът е малък, коректен и съответства на #176.

@ydimitrof

Copy link
Copy Markdown
Contributor

Проверих локално целия diff, историята на PR-а, съответствието с issue #176 и взаимодействието с останалия „chrome" на приложението. Ето обобщението.

Обхват и съответствие с issue #176

PR-ът е rebase на #176 (@Hard-system) върху актуалния main. Имплементацията отговаря на потребителската история дословно: нов клиентски компонент ScrollToTop.tsx, глобално вграден в root.tsx, поява след 400px скрол, плавно връщане нагоре, дизайн токъни (--ink, --accent, --paper), ARIA атрибути и клавиатурен фокус мениджмънт. Пренасянето на CSS-а в styles/chrome.css (вместо в app.css, който вече е само @import) е коректно спрямо split-а от #84. Три файла, 113 добавяния, 0 изтривания — атомарен, фокусиран обхват без scope creep. CI е зелен, prettier е чист.

Сигурност / OWASP

Чисто. Компонентът е изцяло клиентски:

  • Няма injection повърхност — никакъв SQL, никакви заявки, никакъв достъп до registry credentials.
  • Няма XSS векторaria-label е статичен низ, няма dangerouslySetInnerHTML, няма интерполация на потребителски вход. SVG-то е статично.
  • Няма tainted API-та — само window.scrollY, window.scrollTo и addEventListener('scroll', …, { passive: true }).
  • Ресурси: removeEventListener е коректно върнат от useEffect cleanup — няма листенер лийк.
  • Няма нови зависимости, няма тайни, няма мрежови повиквания, няма промяна на URL-и. A11y (aria-hidden + tabIndex=-1 когато скрит + visibility:hidden) е коректно двойно подсигурено.

Не намерих нищо злонамерено или подозрително.

Забележки (не блокиращи)

  1. Възможно застъпване с accessibility launcher на мобилни устройства. На екрани ≤760px третокраният .a11y-tools__button се закача в долния десен ъгъл (layout.css:296top:auto; bottom:0). Новият бутон също стои долу-дясно (bottom:16px; right:16px, 44px). Двата елемента се борят за същия ъгъл на телефон — струва си да се потвърди визуално, че не се припокриват. На десктоп няма конфликт (launcher-ът е на top:420px).

  2. JS smooth-scroll не зачита prefers-reduced-motion. Глобалното правило в base.css:143 неутрализира CSS scroll-behavior и преходите на бутона, но window.scrollTo({ behavior: 'smooth' }) е императивен JS API, който CSS media query не достига (репото изрично се грижи за WCAG 2.3.3 — виж коментара там). Малка добавка с matchMedia('(prefers-reduced-motion: reduce)') за behavior: 'auto' би затворила пропуска.

  3. Без автоматичен тест. Компонентът е тривиален UI, но tests-with-code конвенцията би се удовлетворила от лек RTL тест за прага 400px и toggle-а на is-visible.

Нито една от трите не е блокираща за функционалност или сигурност.

Вердикт: Approve на същество — сигурността е чиста, съответства на issue #176; препоръчвам само да се провери мобилното застъпване с a11y бутона (т.1) преди merge.

@ydimitrof

Copy link
Copy Markdown
Contributor

Одобрено ✅ (Approve на същество)

Ревю на PR #201 — feat(web): add scroll-to-top button

Прегледах целия diff локално, ред по ред, с фокус върху сигурност, интегритет на данните и евентуален зловреден код. PR-ът е малък, изцяло frontend, добавя един презентационен компонент (ScrollToTop.tsx, 53 реда), една закачка в root.tsx (2 реда) и CSS в styles/chrome.css (58 реда). Няма сървърен код, няма достъп до данни.

Сигурност / OWASP

  • SQL injection — неприложимо. Няма заявки, няма достъп до D1, няма динамичен SQL.
  • XSS / инжекция — чисто. Няма dangerouslySetInnerHTML, няма динамичен HTML. Единственият текст е статичен литерал aria-label="Към началото", ескейпнат от React. SVG-то е статично, inline, без потребителски вход.
  • Тайни / URL / зависимости — няма нови пакети, няма нови URL-и, няма ключове или .env/.dev.vars. Нищо за whitelist.
  • Зловреден код — няма обфускация, eval, динамичен import, мрежови заявки или backdoor. window.scrollY / window.scrollTo са единствените браузърни API-та и се ползват по предназначение.
  • SSR безопасностwindow се достъпва само вътре в useEffect и в onClick handler-а, никога по време на render. Няма риск от срив при сървърен рендер.

Интегритет на данните

Неприложимо — компонентът не чете и не пише данни. Няма промяна в state отвъд един локален булев флаг.

Достъпност (силна страна)

aria-hidden={!isVisible}, tabIndex={isVisible ? 0 : -1} и focus-visible стиловете са коректни — бутонът е в DOM постоянно, но е изваден от tab-реда и от screen reader-ите докато е скрит. Добра работа.

Забележки (незадължителни, не блокират)

  1. behavior: 'smooth' игнорира prefers-reduced-motion. Глобалният блок в base.css:144 неутрализира CSS-transition-ите, но window.scrollTo({ behavior: 'smooth' }) е програмен и не се влияе от scroll-behavior: auto !important. Потребител с включен reduced-motion пак ще получи плавно скролиране. Препоръка: проверявайте window.matchMedia('(prefers-reduced-motion: reduce)').matches и подавайте behavior: 'auto' при нужда.
  2. Scroll listener без throttle/rAF. toggleVisibility се извиква на всяко scroll събитие. Listener-ът е passive, а React пропуска ре-render при непроменен state, така че на практика ефектът е пренебрежим — но rAF-обвивка би било по-чисто за много дълги списъци.
  3. z-index: 50 сяда над suggestions popover-а (z-index 40), но под site header-а (z-index 1000) — визуално потвърдено, че не се застъпва с AccessibilityWidget (inline toolbar, не floating). Ок.

Съответствие с описанието

Имплементацията отговаря на потребителската история от #176: fade-in след 400px, плавно връщане, фокус-мениджмънт, дизайн токъни (--ink/--accent), по-голям tap target на мобилно (44px под 760px). Rebase-ът на main и преместването на CSS в styles/chrome.css (вместо app.css, който вече е само @import) са коректно обосновани. Обхватът е един логически.

Верификация

Diff-ът е чист, без TODO/FIXME, без dead code, без дублиране. CI е зелен и prettier минава според нишката за ревю. Забележките по-горе са полиране на UX/a11y и не са пречка за merge.

Благодаря за спретнатата работа и подробното PR описание. 🙏

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

Прегледах rebase-а на #176 (mobile-first + a11y). Компонентът е чист: видимостта е gate-ната за AT и клавиатура (aria-hidden/tabIndex спрямо isVisible), passive scroll listener с cleanup, focus-visible ring, и точно 44px таргет на мобилно (@media max-width:760px). SSR-safe (window само в useEffect).

Една мобилна бележка (не блокер): бутонът е position: fixed долу-вдясно (z-50) — същият ъгъл, който ползват AccessibilityWidget и предстоящият assistant dock (#79/#80, също fixed долу-вдясно). Потвърдете на 360px, че трите fixed контрола не се застъпват (stagger на bottom/right или обща стек-логика). Дребно: базата е 48px, а max-width го смъква на 44px — desktop-first teardown, но и двете стойности работят на телефон, тъй че само за сведение.

Одобрявам (rebase на #176 върху main, prettier-чист). Оригиналът #176 да се затвори след merge.

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

@StanislavBG — rebase-ът вече пази @Hard-system като commit author, добре. За да оцелее авторството и при merge (ако сливането е squash — не мога да проверя настройката с Triage права), добави Co-authored-by trailer на комита:

Co-authored-by: Miro <techhardgreenlife@gmail.com>

След като този PR влезе, ще затворим оригинала #176 (същата функционалност, но разминат с main). Благодаря, че го rebase-на.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR: feat(web): add scroll-to-top button

Обобщение

PR добавя бутон „Към началото“ (ScrollToTop), който се появява след скролване надолу и връща потребителя в горната част на страницата. Реализацията е чиста, съобразена с достъпността (aria-label, aria-hidden, управление на tabIndex, focus-visible стилове, passive scroll listener, respekt към мобилни размери). Няма открити проблеми със сигурността във Фаза 0 — няма твърдо кодирани тайни, нови URL адреси, нови зависимости или зловредни шаблони. Резултат от сигурността: ЧИСТ.

Силни страни

  • Коректна SSR безопасност — window се достъпва само вътре в useEffect, началното състояние е false.
  • Добра достъпност: aria-hidden={!isVisible}, tabIndex превключване, focus-visible стилове, смислен български aria-label.
  • Почистване на event listener в return на useEffect — няма изтичане на ресурси.
  • CSS уважава мобилни размери и използва съществуващите CSS променливи (--ink, --paper, --accent).

Проблеми, изискващи внимание

  1. Липсват тестове (блокиращо спрямо изискванията). Няма добавен нито един тест за новия компонент. Инструкциите изискват покритие ≥90% и смислени тестове за всяка функция. Необходими са тестове за: превключване на видимостта при скрол праг 400px, извикване на scrollTo, ARIA атрибути според състоянието.
  2. Възможно припокриване с AccessibilityWidget. И двата компонента вероятно са position: fixed в долния десен ъгъл. Трябва да се провери визуално, че двата бутона не се застъпват на десктоп и мобилни устройства.
  3. Незадължителна оптимизация на scroll handler-а (rAF/throttle) — незначителна, тъй като React прекъсва повторните рендери при непроменена стойност.

Липсваща документация

Няма актуализация на документация/changelog за новия UI елемент. Ако проектът поддържа такъв, добавете кратко описание.

Заключение

Заявявам REQUEST_CHANGES основно поради липсата на тестове (изискване 3.0/3.0 не е покрито) и необходимостта да се потвърди липсата на визуално припокриване с AccessibilityWidget. Функционалният код е с добро качество и след добавяне на тестове PR може да бъде одобрен.

Comment thread apps/web/app/components/ScrollToTop.tsx Outdated
Comment thread apps/web/app/root.tsx
Comment thread apps/web/app/styles/chrome.css
…tle scroll handler

- pin scroll-to-top to bottom-left instead of bottom-right so it never
  shares a corner with the third-party accessibility launcher, which
  anchors right: 0 at every breakpoint with an unreliable vertical
  position
- throttle the scroll listener with requestAnimationFrame and a ticking
  guard to avoid running toggleVisibility on every scroll event
@StanislavBG

Copy link
Copy Markdown
Contributor Author

Fixed both points, pushed in 2b0d5df:

  • Position collision (chrome.css, root.tsx:192) — .scroll-to-top now pins to bottom-left (was bottom-right) at both breakpoints, since the third-party accessibility launcher anchors right: 0 at every breakpoint with an unreliable vertical position (top: 420px desktop, forced bottom: 0 on mobile). Horizontal separation avoids depending on that widget's quirky vertical placement.
  • Scroll handler perf (ScrollToTop.tsx) — toggleVisibility is now wrapped in requestAnimationFrame with a ticking guard to prevent double-scheduling.

typecheck, test, and format/lint all green.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR: feat(web): add scroll-to-top button

ВЕРДИКТ: COMMENT — чист код без блокиращи проблеми, но липсват тестове и една препоръка за достъпност (prefers-reduced-motion).


Фаза 0 — Сканиране за сигурност (задължително, изпълнено първо)

  • Твърдо кодирани тайни (API ключове/пароли/токени): няма. ✅
  • Промени по URL адреси / нови endpoint-и: няма. ✅
  • Зловреден код (backdoor, инжекция на код, обфускация, eval, dangerouslySetInnerHTML): няма. ✅
  • SQL / NoSQL инжекция: неприложимо — промяната е чисто клиентски UI компонент, не докосва база данни или заявки. ✅
  • XSS: няма динамичен HTML; SVG-то е статично, aria-label е литерал. ✅
  • Нови зависимости: няма — използват се само react и нативни browser API-та. ✅

Резултат от Фаза 0: CLEAN. Няма основание за автоматично REQUEST_CHANGES по сигурност. OWASP: не се откриват релевантни рискове (A03 Injection / A07 / A08) в обхвата на промяната.


Общ преглед на качеството

Компонентът е малък, добре структуриран и следва добри практики:

  • Скролът е throttle-нат чрез requestAnimationFrame + passive: true слушател → без performance регресия. ✅
  • Слушателят се премахва при unmount (removeEventListener) → няма изтичане на ресурси. ✅
  • Достъпност: type="button", aria-label, aria-hidden + tabIndex синхронизирани със състоянието на видимост; при скрит бутон visibility: hidden предотвратява кликане. ✅
  • Позиционирането долу-вляво е документирано с ясен коментар защо не се стакова с third-party a11y бутона. ✅
  • Няма SSR/hydration несъответствие — isVisible стартира с false и на сървъра, и на клиента.

Съответствие с CLAUDE.md / Quality Gates

  • Тестове (изисквани 3.0/3.0):Не са добавени тестове за ScrollToTop. Правилото „COMPREHENSIVE TESTS for every function“ не е изпълнено. Липсва покритие за: превключване на видимостта при праг 400px, throttle логиката и почистването на слушателя. Това е основната причина да не давам APPROVE.
  • Документация: промяната е самодостатъчна; API документация не се изисква. ✅
  • Дублиране / мъртъв код / смесени отговорности: няма. ✅
  • Обхват: атомарна промяна, фокусирана върху една задача, без scope creep. ✅

Забележки (непокриващи, вж. инлайн коментарите)

  1. prefers-reduced-motion не се зачита — гладкият скрол и CSS преходите могат да причинят дискомфорт на потребители с вестибуларни нарушения (WCAG 2.3.3). Препоръчвам да се уважи медия заявката.
  2. Прагът 400 и другите „магически“ стойности биха могли да са именувани константи, но това е дребна стилистична бележка.

Препоръка

Кодът е сигурен и годен за продукция от гледна точка на сигурност/производителност. Преди merge моля добавете unit тестове за компонента и обмислете prefers-reduced-motion. След добавяне на тестове PR-ът може да бъде одобрен.

Comment thread apps/web/app/components/ScrollToTop.tsx Outdated
Comment thread apps/web/app/components/ScrollToTop.tsx Outdated
Comment thread apps/web/app/styles/chrome.css
- honor prefers-reduced-motion by using instant scroll instead of smooth
- disable the button's opacity/transform/background transitions under reduced motion
- extract the 400px show threshold into a named SHOW_AFTER_PX constant

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR: feat(web): add scroll-to-top button

Резюме

Чист и добре структуриран PR, който добавя бутон „Към началото". Кодът е с високо качество: throttling на scroll слушателя през requestAnimationFrame, passive: true, коректно премахване на слушателя при unmount (без изтичане на ресурси), безопасен за SSR (достъпът до window е само в useEffect и в обработчиците, а началното isVisible=false избягва разминаване при хидратация), спазване на prefers-reduced-motion едновременно в JS и в CSS, и добра достъпност (aria-label, управление на aria-hidden/tabIndex). Коментарът в CSS, който обяснява защо бутонът е долу-вляво (за да не се застъпва с third-party accessibility launcher-а вдясно), е много полезен.

Сигурност (Phase 0 — задължителен сканинг)

  • Няма зашити тайни (API ключове, пароли, токени).
  • Няма нови или променени URL адреси.
  • Няма нови зависимости.
  • Няма опасни модели: липсва dangerouslySetInnerHTML, няма потребителски вход, SVG-то е статично и вградено, няма SQL/команден injection повърхнина, няма XSS вектор.
  • OWASP: не се откриват релевантни рискове за този обхват.

Резултат от Phase 0: CLEAN — няма блокиращи проблеми със сигурността.

Забележки (незадължителни)

  1. Липса на тестове. Спрямо стриктните критерии за качество (покритие ≥90% за нов код, смислени тестове) този PR не добавя тестове за новия компонент. За презентационен компонент това е приемливо в много проекти, но за пълно одобрение по вашите правила препоръчвам поне минимален тест (превключване на видимостта над/под прага, извикване на scrollTo, спазване на reduced-motion).
  2. Обхват на скролване. window.scrollY предполага, че страницата скролва през window, а не през вътрешен контейнер. Ако някои изгледи използват скролиращ контейнер, бутонът няма да реагира — струва си да се потвърди, че layout-ът скролва на ниво window.
  3. Козметично (CSS): при :focus-visible box-shadow се презаписва изцяло с фокус-пръстена и се губи основната „повдигната" сянка. Ако е умишлено — окей; иначе комбинирайте двете сенки.

CLAUDE.md / готовност за деплой

  • Атомарна промяна, без scope creep. Реализацията използва минимални промени и съществуващи CSS токени (--ink, --paper, --accent).
  • Без мъртъв код, без дублиране, без смесване на отговорности.
  • Единствената незакрита точка спрямо стриктните gate-ове е покритието с тестове.

Вердикт на отделен ред за бърз преглед от други ревюъри:

ВЕРДИКТ: COMMENT — сигурността е чиста и кодът е с високо качество; преди пълно одобрение добавете тестове за новия компонент (и потвърдете обхвата на скролване).

Comment thread apps/web/app/components/ScrollToTop.tsx
Comment thread apps/web/app/components/ScrollToTop.tsx
Comment thread apps/web/app/styles/chrome.css
Covers visibility toggling, scrollTo invocation, and reduced-motion
behavior for ScrollToTop per ydimitrof's review on midt-bg#201. Also
preserves the button's raised drop-shadow under :focus-visible
(previously overwritten by the focus-ring box-shadow) and documents
the document/window-level scroll assumption.
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

За атрибуция (on-record): първоначалната реализация на този scroll-to-top компонент е от #176 (@Hard-system). Този PR стъпва на нея и я надгражда (rAF throttling, prefers-reduced-motion, тестове). При merge @Hard-system се кредитира като съавтор чрез Co-authored-by в squash commit-а — оригиналното авторство е запазено.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Забележка: Това е чернова за преглед. Не са публикувани inline коментари — изчаквам твоята проверка, преди да изпратим нещо към автора.


🇧🇬 Български вариант

Резюме на прегледа — feat(web): add scroll-to-top button

Прегледах промените стриктно, с приоритет върху сигурност и цялост на данните.

Фаза 0 — Сканиране за сигурност: ЧИСТО

  • Няма зашити тайни (API ключове, пароли, токени).
  • Няма добавени или променени URL адреси; няма мрежови заявки.
  • Няма SQL, няма eval/Function, няма dangerouslySetInnerHTML — SVG е статичен inline маркъп. Няма вектори за инжекции/XSS.
  • Единствената нова зависимост е jsdom (само за разработка/тестове). Всички добавки в pnpm-lock.yaml са легитимни транзитивни зависимости на jsdom. Няма признаци за компрометирана верига на доставки. OWASP: няма отклонения.

Коректност: Много добра. useEffect работи само на клиента (SSR-безопасно; началното isVisible=false съвпада със сървърния рендер → няма hydration несъответствие). Scroll слушателят е passive и throttle-нат през requestAnimationFrame с ticking флаг. Cleanup функцията коректно премахва слушателя (няма изтичане на ресурси). Reduced-motion е спазено и в JS (matchMedia), и в CSS.

Достъпност: aria-hidden и tabIndex се превключват заедно правилно; visibility: hidden спира кликовете, когато бутонът е скрит; има aria-label; декоративният SVG е с aria-hidden="true". Отлично.

Дребни, неблокиращи бележки:

  1. Граничната стойност точно на SHOW_AFTER_PX (400, строго >) не е покрита с тест — тества се 399 и 401, но не и 400.
  2. Пътят на почистване при unmount (премахване на слушателя) не е тестван.
  3. Тестовете извикват .click() докато бутонът е логически скрит (scrollY=0) — това упражнява handler-а, но не реалната pointer семантика при visibility: hidden.

Съответствие с CLAUDE.md: Няма частична имплементация, TODO-та, дублиран или мъртъв код. Именуването е консистентно. Разделението на отговорностите е чисто (компонент + стилове + тестове).

ВЕРДИКТ: COMMENT — няма блокиращи или свързани със сигурността проблеми; препоръчвам да се добавят 2 малки теста (граница 400 и cleanup при unmount) преди merge.


🇬🇧 English version

Review summary — feat(web): add scroll-to-top button

I reviewed the changes strictly, prioritizing security and data integrity.

Phase 0 — Security scan: CLEAN

  • No hardcoded secrets (API keys, passwords, tokens).
  • No new/modified URLs; no network calls introduced.
  • No SQL, no eval/Function, no dangerouslySetInnerHTML — the SVG is static inline markup. No injection/XSS vectors.
  • The only new dependency is jsdom (dev/test only). All pnpm-lock.yaml additions are its legitimate transitive deps. No supply-chain red flags. OWASP: no deviations.

Correctness: Strong. useEffect is client-only (SSR-safe; initial isVisible=false matches the server render → no hydration mismatch). The scroll listener is passive and rAF-throttled with a ticking guard. The cleanup removes the listener (no resource leak). Reduced motion is respected in both JS (matchMedia) and CSS.

Accessibility: aria-hidden and tabIndex toggle together correctly; visibility: hidden blocks clicks while hidden; aria-label present; decorative SVG marked aria-hidden="true". Excellent.

Minor, non-blocking notes:

  1. The exact SHOW_AFTER_PX boundary (400, strict >) is untested — 399 and 401 are covered, but not 400.
  2. The unmount cleanup path (listener removal) is untested.
  3. Tests call .click() while the button is logically hidden (scrollY=0) — this exercises the handler but not real pointer semantics under visibility: hidden.

CLAUDE.md compliance: No partial implementation, TODOs, duplicated or dead code. Naming is consistent. Concerns are cleanly separated (component + styles + tests).

VERDICT: COMMENT — no blocking or security issues; recommend adding 2 small tests (the 400 boundary and unmount cleanup) before merge.

@todorkolev
todorkolev dismissed ydimitrof’s stale review July 15, 2026 13:39

И трите бележки са адресирани в последвалите комити: тестове за компонента (de85a1f), разделяне от accessibility widget-а + rAF throttle (2b0d5df), reduced-motion (d27f9cf). Всички threads са разрешени.

@todorkolev todorkolev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Прегледано и тествано локално: rAF-throttle на scroll handler-а, prefers-reduced-motion в JS и CSS, позициониране вляво без колизия с accessibility widget-а, тестове за компонента. Бележките на ydimitrof са адресирани в комитите от 10-11.07.

@todorkolev
todorkolev merged commit 5a7c5c6 into midt-bg:main Jul 15, 2026
1 check passed
@todorkolev todorkolev mentioned this pull request Jul 15, 2026
12 tasks
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.

5 participants