Skip to content

Common UI components refactoring & accesibility improvements - #525

Merged
Blaumaus merged 7 commits into
mainfrom
improvement/ui-refactor
Apr 30, 2026
Merged

Blaumaus merged 7 commits into
mainfrom
improvement/ui-refactor

Conversation

@Blaumaus

@Blaumaus Blaumaus commented Apr 30, 2026 •

Copy link
Copy Markdown
Member

Changes

If applicable, please describe what changes were made in this pull request.

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

Summary by CodeRabbit

  • New Features

    • Card-style radio selection added.
  • UI/Style Updates

    • Unified button sizing/variants and standardized typography across many pages, modals, forms, and empty states.
    • Visual tweaks to badges, checkboxes, inputs, tooltips, dropdowns, loaders and other primitives.
  • Accessibility Improvements

    • Better ARIA roles/labels, keyboard navigation, live-region semantics, and improved date input accessibility.

@Blaumaus Blaumaus self-assigned this Apr 30, 2026
@coderabbitai

coderabbitai Bot commented Apr 30, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d06afde0-0afb-450f-a51b-3b6c6eb85aff

📥 Commits

Reviewing files that changed from the base of the PR and between d3244d9 and ba72e7f.

📒 Files selected for processing (4)
  • web/app/pages/Organisations/Settings/People.tsx
  • web/app/pages/Project/View/components/RefreshStatsButton.tsx
  • web/app/root.tsx
  • web/app/ui/ProgressRing.tsx

📝 Walkthrough

Walkthrough

Centralizes Button styling into a new variant/size API and buttonClasses; replaces many raw headings/paragraphs with a shared Text component; adds a typed RadioCardGroup; and applies broad styling, accessibility, and ARIA enhancements across numerous UI primitives and pages.

Changes

Cohort / File(s) Summary
Button core
web/app/ui/Button.tsx
Replaces legacy boolean props with variant/size, adds exported buttonClasses helper, centralizes class generation, and simplifies rendering to children.
Button usages (global)
web/app/components/..., web/app/pages/**, web/app/routes/**
Updated many call-sites to use variant/size (auth, onboarding, dashboards, tools, project pages); converted native/icon <button> to variant='icon'; normalized icon sizing. Review for any missed boolean prop remnants.
Typography → Text
web/app/ui/Text.tsx, web/app/pages/**, web/app/components/**, web/app/routes/**
Replaced numerous raw h*/p/label usages with Text; expanded as types and adjusted colour/code variants. Check visual parity and semantic tags.
New component
web/app/ui/RadioCardGroup.tsx
Adds typed RadioCardGroup<T> (label/hint/options/error/disabled/joined). Inspect usages where radio fieldsets were replaced.
Primitives: ARIA & behavior
web/app/ui/{Alert,Badge,Checkbox,Combobox,Input,Select,Textarea,Tooltip,Modal,Loader,LoadingBar,MultiProgress,MultiSelect,Pagination,Datepicker,ProgressRing,PasswordStrength,Switch,FileUpload,Dropdown,Drawer,FAQ,CodeBlock}
Styling and accessibility updates: ARIA roles/status/progress, keyboard improvements (MultiSelect, Pagination), new props (Badge.size, Checkbox.indeterminate, Loader.label, Datepicker.inputId/ariaLabelledBy, ProgressRing.ariaLabel, MultiProgress ARIA). Review ARIA/keyboard changes and prop typings.
Modals & small components
web/app/modals/*, web/app/components/NotificationChannels/*
Switched native buttons to shared Button, replaced inline info blocks with Alert, normalized loading/disabled wiring and used useId() for Datepicker labels in modals.
Project / Settings / Feature pages
web/app/pages/Project/**, web/app/pages/Organisations/**, web/app/pages/UserSettings/**
Empty-state UIs refactored to Text + Button; radio fieldsets replaced by RadioCardGroup; many buttons standardized to variant/size; some embedded-mode query handling removed.
Tools & calculators
web/app/routes/tools.*.tsx, other tools routes
Headings converted to Text; Reset/submit buttons switched to variant='secondary' or explicit size; minor helpers added (e.g., getHostnameFromUrl).
UI library tweaks
web/app/ui/{Badge.tsx,Checkbox.tsx,Datepicker.tsx,Loader.tsx,MultiProgress.tsx,ProgressRing.tsx}
Public prop updates: Badge.size, Checkbox.indeterminate, Datepicker.inputId/ariaLabelledBy, Loader.label, MultiProgress ARIA props, ProgressRing.ariaLabel. Confirm export types.
Localization
web/public/locales/{en,de,fr,pl,uk}.json
Adds pagination-related keys (toggleOptions, paginationNavLabel, jumpToPage, pageNumber) to common.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Poem

🐰 I swapped old props for variant and size,
I hopped through Text to make headings wise,
Radio cards align in tidy rows,
Icons shrink where the new Button goes,
A joyful rabbit hums — the UI glows ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the primary change: a comprehensive refactoring of common UI components with accessibility improvements across the codebase.
Description check ✅ Passed The PR description follows the repository template structure and completes required sections, though it lacks detailed explanation of specific changes made in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch improvement/ui-refactor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@Blaumaus
Blaumaus marked this pull request as ready for review April 30, 2026 02:11

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (5)
web/app/ui/Pagination.tsx (2)

132-143: ⚡ Quick win

Avoid duplicating the DOTS jump-page calculation.

middlePage is computed in onClick and recomputed again for aria-label. Extract it once and reuse it so click behavior and announced target cannot drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/ui/Pagination.tsx` around lines 132 - 143, The DOTS jump computes the
same middle page twice which can drift; compute the middle page once and reuse
it for both the click handler and aria-label. In the Pagination component,
extract the calculation of middlePage (from paginationRange[index - 1] and
paginationRange[index + 1]) into a single const (e.g., middlePage) in the
surrounding scope for that DOTS element, then call setPage(middlePage) in the
onClick and use that same middlePage value in the aria-label string; update
references to paginationRange, setPage, onClick, and aria-label accordingly so
they share the single computed middlePage.

111-112: 🏗️ Heavy lift

Localize the new ARIA labels instead of hardcoding English.

Line 111 ('Pagination'), Line 142 (Jump to page ...), and Line 157 (Page ...) bypass t(...). This creates mixed-language screen reader output in localized UIs.

Also applies to: 142-143, 157-157

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/ui/Pagination.tsx` around lines 111 - 112, The ARIA labels in the
Pagination component are hardcoded English strings; replace the literals
"'Pagination'", "Jump to page ..." and "Page ..." with localized strings via the
existing i18n translator (e.g., call t('pagination.ariaLabel') for the
container, t('pagination.jumpTo', { page }) for the jump input aria-label, and
t('pagination.page', { page }) for page labels). Update the import/use of the
translator in the Pagination component (e.g., useTranslation or t from the
project's i18n helper) and add appropriate i18n keys with interpolation for the
page number (pagination.ariaLabel, pagination.jumpTo, pagination.page) so screen
readers receive localized text.
web/app/pages/UserSettings/components/Organisations.tsx (1)

105-113: ⚡ Quick win

Give reject a destructive variant for safer UX.

At Line 106 and Line 112, reject and accept both appear primary-styled. Adding a destructive variant to reject preserves action contrast.

Proposed tweak
             <Button
+              variant='danger-outline'
               size='xs'
               className='mr-2'
               onClick={() => setShowDeleteModal(true)}
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/pages/UserSettings/components/Organisations.tsx` around lines 105 -
113, The "reject" Button currently uses the same primary styling as "accept";
update the first Button (the one that calls setShowDeleteModal(true)) to use a
destructive variant (e.g., add variant="destructive") so the reject action is
visually distinct and safer; leave the accept Button (onAccept, isLoading)
styled as primary or unchanged. This change targets the Button element that
calls setShowDeleteModal and the nearby onAccept/isLoading usage for context.
web/app/pages/UserSettings/components/ProjectList.tsx (1)

103-112: ⚡ Quick win

Keep reject action visually distinct from accept.

At Line 104 and Line 111, both actions render as primary-styled buttons. This weakens destructive-action affordance and increases misclick risk.

Proposed tweak
-            <Button
-              size='xs'
-              className='mr-2'
-              onClick={() => setShowDeleteModal(true)}
-              loading={isPending}
-            >
+            <Button
+              variant='danger-outline'
+              size='xs'
+              className='mr-2'
+              onClick={() => setShowDeleteModal(true)}
+              loading={isPending}
+            >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/pages/UserSettings/components/ProjectList.tsx` around lines 103 -
112, The reject and accept buttons in ProjectList are visually similar; change
the reject button (the one that calls setShowDeleteModal(true) and uses
isPending) to a distinct destructive/secondary style (e.g., use the component's
danger/secondary/ghost variant or a different color prop) while leaving the
accept button (onAccept, isPending) as primary; ensure any className or size
props remain but update the Button variant/intent for the reject action so it
clearly contrasts with the accept action.
web/app/ui/StatusPage.tsx (1)

68-75: ⚡ Quick win

Use the shared buttonClasses() helper here.

web/app/ui/Button.tsx already exports buttonClasses() for exactly this case. Re-encoding the primary/secondary button styles in StatusPage means this page can drift the next time the Button API or focus treatment changes.

♻️ Proposed refactor
 import { Link } from '~/ui/Link'
+import { buttonClasses } from '~/ui/Button'
 
 import { cn } from '~/utils/generic'
@@
-const baseActionClasses =
-  'inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.985] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden'
-
-const primaryActionClasses =
-  'bg-slate-900 text-white shadow-xs hover:bg-slate-700 focus-visible:ring-slate-900 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white dark:focus-visible:ring-slate-300 dark:focus-visible:ring-offset-slate-950'
-
-const secondaryActionClasses =
-  'bg-white text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-slate-900 dark:bg-slate-900 dark:text-gray-200 dark:ring-slate-700/80 dark:hover:bg-slate-800 dark:focus-visible:ring-slate-300 dark:focus-visible:ring-offset-slate-950'
-
@@
-                  className={cn(
-                    baseActionClasses,
-                    action.primary
-                      ? primaryActionClasses
-                      : secondaryActionClasses,
-                  )}
+                  className={buttonClasses({
+                    variant: action.primary ? 'primary' : 'secondary',
+                  })}
@@
-                  className={cn(
-                    baseActionClasses,
-                    action.primary
-                      ? primaryActionClasses
-                      : secondaryActionClasses,
-                  )}
+                  className={buttonClasses({
+                    variant: action.primary ? 'primary' : 'secondary',
+                  })}

Also applies to: 145-149, 160-163

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/ui/StatusPage.tsx` around lines 68 - 75, StatusPage.tsx duplicates
button styling via baseActionClasses, primaryActionClasses, and
secondaryActionClasses; replace those duplicated class strings by importing and
using the shared buttonClasses() helper from Button.tsx (call buttonClasses({
variant: 'primary' }) and buttonClasses({ variant: 'secondary' }) or the
appropriate API) and remove the three constants; update all usages (including
the other occurrences noted around lines ~145-149 and ~160-163) to call
buttonClasses() so the page stays in sync with the central Button styles and
focus behaviors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/app/pages/Project/Settings/Alerts/ProjectAlertsView.tsx`:
- Around line 427-451: The no-permission fallback in ProjectAlertsView currently
shows a signup CTA even when an authenticated non-owner hits the block; update
the rendering logic around the Link that uses routes.signup so it only renders
the signup CTA for unauthenticated users (e.g., check the app's auth state such
as currentUser or isAuthenticated from context/props) and for authenticated
users replace it with a more appropriate action (like a disabled button, an
upgrade/ownership info link, or nothing). Locate the block guarded by
canManageAlerts and conditionally render the Link to routes.signup based on the
auth check, leaving the rest of the markup (icons/text) unchanged.

In `@web/app/pages/Project/View/components/RefreshStatsButton.tsx`:
- Around line 100-105: The Button disabled prop only checks authLoading but
handleManualRefresh also no-ops when dataLoading or isRefreshing; update the
Button in RefreshStatsButton to set disabled to (authLoading || dataLoading ||
isRefreshing) so the UI matches the click guard in handleManualRefresh and
prevents a clickable control that does nothing.

In `@web/app/ui/Combobox.tsx`:
- Around line 69-72: The hardcoded aria-label on the ComboboxButton ("Toggle
options") needs to be replaced with a localized string; update the
ComboboxButton in the Combobox component to use the app's i18n translation
(e.g., call the existing translation hook/function used in this file such as t
or useTranslation) and pass a translation key like "combobox.toggleOptions" (or
the matching key in your locale files) instead of the hardcoded text so the
aria-label is localized for assistive technologies.

In `@web/app/ui/FileUpload.tsx`:
- Around line 119-124: The icon wrapper still includes group-hover styles even
when the dropzone is disabled; update the className construction in
FileUpload.tsx (the JSX element using cn with dragOver) to include the
group-hover:* classes only when !isDisabled, i.e., conditionally append the
hover-related class strings behind a !isDisabled check so that when isDisabled
is true no group-hover styles are applied; reference the className usage, the
dragOver variable and isDisabled prop to locate and change the conditional
strings.

In `@web/app/ui/Input.tsx`:
- Around line 95-101: The password-visibility toggle button still responds to
clicks when the input is disabled/readOnly; update the button in Input.tsx (the
element using onClick, aria-pressed, tabIndex) to be non-interactive when
disabled or readOnly by adding disabled={disabled || readOnly} and
aria-disabled={disabled || readOnly}, and guard the onClick handler to return
early when disabled || readOnly before calling setShowPassword(!showPassword);
this ensures the toggle cannot be activated while the input is
disabled/readOnly.

In `@web/app/ui/MultiProgress.tsx`:
- Around line 23-29: The MultiProgress component currently sets
role='progressbar' on the wrapper but exposes no accessible name or value;
either remove the role to make the bar purely decorative or add props to surface
ARIA metadata so callers can supply semantics. Update MultiProgress to accept
optional props such as aria-label, aria-labelledby, aria-valuenow,
aria-valuemin, aria-valuemax (and/or an isDecorative boolean) and only render
role='progressbar' when a name/value are provided; otherwise omit role (or set
aria-hidden) so screen readers aren't given an unlabeled control. Ensure the
change is applied where role and className are set and document the new props in
the component API.

In `@web/app/ui/MultiSelect.tsx`:
- Around line 168-178: The listbox div (identified by id={listboxId},
role='listbox' and controlled by isOpen) is only opacity-hidden and remains in
the accessibility tree and tabbable when closed; change the render so the
listbox is removed from the DOM when closed (either by conditional rendering
based on isOpen or by replacing the wrapper with a transition component that
unmounts after exit) so it cannot receive Tab focus while collapsed; apply the
same change to the other popup block that uses isOpen (the similar wrapper
around lines 190-198) to ensure both popups are unmounted when closed and keep
any close animation by using an unmount-on-exit transition.

In `@web/app/ui/PasswordStrength.tsx`:
- Around line 85-94: The progressbar is missing aria-valuetext so screen readers
only get a number; update the PasswordStrength component to set aria-valuetext
to the human-readable strength label (the same localized label shown visually)
instead of only numeric state. Use the existing strength label source (e.g. the
value derived from currentConfig such as currentConfig.label or the localized
string returned by t(...)) and pass it into the div as
aria-valuetext={strengthLabel} while keeping
aria-valuenow={currentConfig.segments} so assistive tech reads both the number
and the textual strength.

In `@web/app/ui/ProgressRing.tsx`:
- Around line 94-99: The progressbar can be rendered without an accessible name
and announces completion early because aria-label is optional and aria-valuenow
uses the rounded value; in ProgressRing update the accessibility attributes so
aria-label always has a fallback (e.g., use ariaLabel || `Progress:
${Math.floor(value)}%` or similar descriptive string) and set aria-valuenow to a
non-inflated numeric value (use the raw/clamped value or Math.floor(value)
instead of rounded) to avoid prematurely announcing 100; apply the same fix for
the other instance referenced (the attributes around lines 132-133) and keep
references to the ProgressRing props (value, rounded, ariaLabel) when making the
change.

In `@web/app/ui/Switch.tsx`:
- Around line 53-67: Thumb contrast in dark mode is lost because the thumb's
static classes ('pointer-events-none inline-block size-3 ... bg-white') always
set it white while the checked dark-mode track uses a light color; update the
thumb class logic in Switch.tsx (the JSX element using the 'pointer-events-none
inline-block ...' class and the checked boolean) to conditionally apply a
dark-mode thumb color when checked (e.g., add a conditional 'dark:...' class or
use cx to apply a different thumb background when checked) so the thumb
contrasts against the checked dark-mode track and maintains visible state.

---

Nitpick comments:
In `@web/app/pages/UserSettings/components/Organisations.tsx`:
- Around line 105-113: The "reject" Button currently uses the same primary
styling as "accept"; update the first Button (the one that calls
setShowDeleteModal(true)) to use a destructive variant (e.g., add
variant="destructive") so the reject action is visually distinct and safer;
leave the accept Button (onAccept, isLoading) styled as primary or unchanged.
This change targets the Button element that calls setShowDeleteModal and the
nearby onAccept/isLoading usage for context.

In `@web/app/pages/UserSettings/components/ProjectList.tsx`:
- Around line 103-112: The reject and accept buttons in ProjectList are visually
similar; change the reject button (the one that calls setShowDeleteModal(true)
and uses isPending) to a distinct destructive/secondary style (e.g., use the
component's danger/secondary/ghost variant or a different color prop) while
leaving the accept button (onAccept, isPending) as primary; ensure any className
or size props remain but update the Button variant/intent for the reject action
so it clearly contrasts with the accept action.

In `@web/app/ui/Pagination.tsx`:
- Around line 132-143: The DOTS jump computes the same middle page twice which
can drift; compute the middle page once and reuse it for both the click handler
and aria-label. In the Pagination component, extract the calculation of
middlePage (from paginationRange[index - 1] and paginationRange[index + 1]) into
a single const (e.g., middlePage) in the surrounding scope for that DOTS
element, then call setPage(middlePage) in the onClick and use that same
middlePage value in the aria-label string; update references to paginationRange,
setPage, onClick, and aria-label accordingly so they share the single computed
middlePage.
- Around line 111-112: The ARIA labels in the Pagination component are hardcoded
English strings; replace the literals "'Pagination'", "Jump to page ..." and
"Page ..." with localized strings via the existing i18n translator (e.g., call
t('pagination.ariaLabel') for the container, t('pagination.jumpTo', { page })
for the jump input aria-label, and t('pagination.page', { page }) for page
labels). Update the import/use of the translator in the Pagination component
(e.g., useTranslation or t from the project's i18n helper) and add appropriate
i18n keys with interpolation for the page number (pagination.ariaLabel,
pagination.jumpTo, pagination.page) so screen readers receive localized text.

In `@web/app/ui/StatusPage.tsx`:
- Around line 68-75: StatusPage.tsx duplicates button styling via
baseActionClasses, primaryActionClasses, and secondaryActionClasses; replace
those duplicated class strings by importing and using the shared buttonClasses()
helper from Button.tsx (call buttonClasses({ variant: 'primary' }) and
buttonClasses({ variant: 'secondary' }) or the appropriate API) and remove the
three constants; update all usages (including the other occurrences noted around
lines ~145-149 and ~160-163) to call buttonClasses() so the page stays in sync
with the central Button styles and focus behaviors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7d11e94-0329-4ad4-b890-369a4973b8bc

📥 Commits

Reviewing files that changed from the base of the PR and between cdd4926 and b6fe522.

📒 Files selected for processing (92)
  • web/app/components/GithubAuth.tsx
  • web/app/components/GoogleAuth.tsx
  • web/app/components/NotificationChannels/EnableWebPushButton.tsx
  • web/app/components/NotificationChannels/NotificationChannels.tsx
  • web/app/components/OIDCAuth.tsx
  • web/app/components/pricing/BillingPricing.tsx
  • web/app/modals/AnnotationModal.tsx
  • web/app/modals/NewFunnel.tsx
  • web/app/pages/Auth/CreateNewPassword/CreateNewPassword.tsx
  • web/app/pages/Auth/ForgotPassword/ForgotPassword.tsx
  • web/app/pages/Auth/Signin/Signin.tsx
  • web/app/pages/Auth/Signup/InvitationSignup.tsx
  • web/app/pages/Auth/Signup/Signup.tsx
  • web/app/pages/Captcha/CaptchaDemo.tsx
  • web/app/pages/Captcha/index.tsx
  • web/app/pages/Dashboard/Dashboard.tsx
  • web/app/pages/Dashboard/NoProjects.tsx
  • web/app/pages/Dashboard/PeriodSelector.tsx
  • web/app/pages/Dashboard/SortSelector.tsx
  • web/app/pages/Onboarding/Onboarding.tsx
  • web/app/pages/Organisations/Organisations.tsx
  • web/app/pages/Organisations/Settings/People.tsx
  • web/app/pages/Organisations/Settings/Projects.tsx
  • web/app/pages/Organisations/Settings/index.tsx
  • web/app/pages/Project/Settings/Alerts/AlertTemplateEditor.tsx
  • web/app/pages/Project/Settings/Alerts/ProjectAlertsSettings.tsx
  • web/app/pages/Project/Settings/Alerts/ProjectAlertsView.tsx
  • web/app/pages/Project/Settings/Annotations.tsx
  • web/app/pages/Project/Settings/Emails/Emails.tsx
  • web/app/pages/Project/Settings/People/People.tsx
  • web/app/pages/Project/Settings/ProjectSettings.tsx
  • web/app/pages/Project/Settings/components/DataImportTab.tsx
  • web/app/pages/Project/Settings/components/ProxyDomainsTab.tsx
  • web/app/pages/Project/Settings/tabs/DangerZone.tsx
  • web/app/pages/Project/Settings/tabs/Revenue.tsx
  • web/app/pages/Project/View/ViewProject.tsx
  • web/app/pages/Project/View/components/ChartTypeSwitcher.tsx
  • web/app/pages/Project/View/components/DashboardHeader.tsx
  • web/app/pages/Project/View/components/PasswordRequiredModal.tsx
  • web/app/pages/Project/View/components/RefreshStatsButton.tsx
  • web/app/pages/Project/tabs/AskAI/AskAIView.tsx
  • web/app/pages/Project/tabs/Errors/ErrorDetails.tsx
  • web/app/pages/Project/tabs/Experiments/ExperimentResults.tsx
  • web/app/pages/Project/tabs/Experiments/ExperimentSettingsModal.tsx
  • web/app/pages/Project/tabs/Experiments/ExperimentsView.tsx
  • web/app/pages/Project/tabs/FeatureFlags/FeatureFlagSettingsModal.tsx
  • web/app/pages/Project/tabs/FeatureFlags/FeatureFlagsView.tsx
  • web/app/pages/Project/tabs/Funnels/FunnelsView.tsx
  • web/app/pages/Project/tabs/Goals/GoalSettingsModal.tsx
  • web/app/pages/Project/tabs/Goals/GoalsView.tsx
  • web/app/pages/Project/tabs/Profiles/ProfileDetails.tsx
  • web/app/pages/Subscribe/Subscribe.tsx
  • web/app/pages/UserSettings/UserSettings.tsx
  • web/app/pages/UserSettings/components/Organisations.tsx
  • web/app/pages/UserSettings/components/ProjectList.tsx
  • web/app/pages/UserSettings/components/Socialisations.tsx
  • web/app/pages/UserSettings/components/TwoFA.tsx
  • web/app/routes/tools.conversion-rate-calculator.tsx
  • web/app/routes/tools.ctr-calculator.tsx
  • web/app/routes/tools.ip-lookup.tsx
  • web/app/routes/tools.percentage-calculator.tsx
  • web/app/routes/tools.qr-code-generator.tsx
  • web/app/routes/tools.robots-txt-generator.tsx
  • web/app/routes/tools.schema-markup-generator.tsx
  • web/app/routes/tools.sitemap-validator.tsx
  • web/app/routes/tools.utm-generator.tsx
  • web/app/ui/Alert.tsx
  • web/app/ui/Badge.tsx
  • web/app/ui/Button.tsx
  • web/app/ui/Checkbox.tsx
  • web/app/ui/CodeBlock.tsx
  • web/app/ui/Combobox.tsx
  • web/app/ui/Drawer.tsx
  • web/app/ui/Dropdown.tsx
  • web/app/ui/FAQ.tsx
  • web/app/ui/FileUpload.tsx
  • web/app/ui/Input.tsx
  • web/app/ui/Loader.tsx
  • web/app/ui/LoadingBar.tsx
  • web/app/ui/Modal.tsx
  • web/app/ui/MultiProgress.tsx
  • web/app/ui/MultiSelect.tsx
  • web/app/ui/Pagination.tsx
  • web/app/ui/PasswordStrength.tsx
  • web/app/ui/ProgressRing.tsx
  • web/app/ui/RadioCardGroup.tsx
  • web/app/ui/Select.tsx
  • web/app/ui/StatusPage.tsx
  • web/app/ui/Switch.tsx
  • web/app/ui/Text.tsx
  • web/app/ui/Textarea.tsx
  • web/app/ui/Tooltip.tsx
💤 Files with no reviewable changes (4)
  • web/app/routes/tools.ip-lookup.tsx
  • web/app/pages/Project/tabs/Profiles/ProfileDetails.tsx
  • web/app/routes/tools.qr-code-generator.tsx
  • web/app/routes/tools.sitemap-validator.tsx

Comment thread web/app/ui/Combobox.tsx Resolved
Comment thread web/app/ui/FileUpload.tsx Resolved
Comment thread web/app/ui/Input.tsx Resolved
Comment thread web/app/ui/MultiProgress.tsx Resolved
Comment thread web/app/ui/MultiSelect.tsx Resolved
Comment thread web/app/ui/PasswordStrength.tsx Resolved
Comment thread web/app/ui/ProgressRing.tsx Resolved
Comment thread web/app/ui/Switch.tsx Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
web/app/pages/Project/View/components/RefreshStatsButton.tsx (1)

55-63: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ensure refresh state always resets on failed refresh calls.

Both auto and manual paths can leave isRefreshing stuck true if onRefresh(...) throws, and the auto path can also produce an unhandled rejected promise. Wrap both awaits with try/finally (and catch/log in auto path) so UI state is always restored.

Suggested fix
       if (
         newProgress >= 100 &&
         !hasTriggeredRefresh.current &&
         !isRefreshing &&
         !authLoading &&
         !dataLoading
       ) {
         hasTriggeredRefresh.current = true
         ;(async () => {
           setIsRefreshing(true)
-          await onRefresh(false)
-          setIsRefreshing(false)
-          // Reset timer after refresh completes
-          startTimeRef.current = Date.now()
-          hasTriggeredRefresh.current = false
-          setProgress(0)
+          try {
+            await onRefresh(false)
+          } catch (error) {
+            console.error(error)
+          } finally {
+            setIsRefreshing(false)
+            // Reset timer after refresh completes
+            startTimeRef.current = Date.now()
+            hasTriggeredRefresh.current = false
+            setProgress(0)
+          }
         })()
       }
@@
   const handleManualRefresh = async () => {
     if (authLoading || dataLoading || isRefreshing) return
     setIsRefreshing(true)
     startTimeRef.current = Date.now()
     hasTriggeredRefresh.current = false
     setProgress(0)
-    await onRefresh(true)
-    setIsRefreshing(false)
+    try {
+      await onRefresh(true)
+    } finally {
+      setIsRefreshing(false)
+    }
   }

Also applies to: 74-82

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/pages/Project/View/components/RefreshStatsButton.tsx` around lines 55
- 63, The refresh logic can leave isRefreshing stuck true and produce unhandled
rejections if onRefresh throws; wrap the await calls to onRefresh(false) (manual
path inside the IIFE) and the auto-refresh await (the block around lines using
startTimeRef/hasTriggeredRefresh/setProgress) in try/finally so
setIsRefreshing(false), startTimeRef.current reset, hasTriggeredRefresh.current
= false and setProgress(0) always run, and additionally catch/log errors in the
auto path to avoid unhandled promise rejections; locate the logic around the
anonymous async IIFE and the auto-refresh block that calls onRefresh and apply
try/finally (with a catch in the auto path) to restore UI state on failure.
web/app/root.tsx (1)

255-275: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add disclosure ARIA state for crash stack toggle.

The show/hide stack button should expose expanded state and control target so screen readers can understand the toggle behavior.

Suggested fix
-                      <button
+                      <button
                         type='button'
                         onClick={() => setCrashStackShown((prev) => !prev)}
+                        aria-expanded={crashStackShown}
+                        aria-controls='error-boundary-stack-trace'
                         className='flex cursor-pointer items-center justify-center text-base text-gray-800 hover:underline dark:text-gray-300'
                       >
@@
-                        <span className='text-sm whitespace-pre-line text-gray-600 dark:text-gray-400'>
+                        <span
+                          id='error-boundary-stack-trace'
+                          className='text-sm whitespace-pre-line text-gray-600 dark:text-gray-400'
+                        >
                           {error.stack}
                         </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/root.tsx` around lines 255 - 275, The crash stack toggle button
(using setCrashStackShown and crashStackShown) lacks ARIA disclosure
attributes—add aria-expanded={crashStackShown} to the button and give the stack
container (the span rendering error.stack) a stable id (e.g., crash-stack) and
add aria-controls pointing to that id on the button; ensure the controlled span
has role="region" or aria-hidden appropriately so screen readers can detect the
expanded/collapsed state and target.
web/app/routes/_index.tsx (1)

335-345: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make live-demo overlay keyboard-accessible (not hover-only).

The CTA reveal is tied to group-hover; add group-focus-within so keyboard focus can reveal the overlay too.

Suggested fix
-        <div className='pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl bg-slate-900/40 opacity-0 backdrop-blur-[2px] transition-opacity duration-200 group-hover:pointer-events-auto group-hover:opacity-100'>
+        <div className='pointer-events-none absolute inset-0 flex items-center justify-center rounded-2xl bg-slate-900/40 opacity-0 backdrop-blur-[2px] transition-opacity duration-200 group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100'>
           <a
             href={LIVE_DEMO_URL}
             target='_blank'
             rel='noopener noreferrer'
-            className='pointer-events-auto inline-flex items-center rounded-md bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-black/10 transition-all hover:bg-gray-50 dark:bg-slate-950 dark:text-white dark:ring-white/10 dark:hover:bg-slate-900'
+            className='pointer-events-auto inline-flex items-center rounded-md bg-white px-4 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-black/10 transition-all hover:bg-gray-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 dark:bg-slate-950 dark:text-white dark:ring-white/10 dark:hover:bg-slate-900'
             aria-label={`${t('main.seeLiveDemo')} (opens in a new tab)`}
           >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/routes/_index.tsx` around lines 335 - 345, The overlay reveal is
currently only triggered by hover via the "group-hover" tailwind classes on the
overlay div; update that overlay's class list (the div containing the CTA anchor
and ArrowRightIcon) to also include the "group-focus-within" variants (e.g., add
group-focus-within:pointer-events-auto and group-focus-within:opacity-100) so
keyboard focus on the live demo link (LIVE_DEMO_URL anchor) will reveal the
overlay as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/app/modals/AnnotationModal.tsx`:
- Around line 125-135: The label rendered by the Text component is not linked to
the Datepicker input; generate a unique id (e.g., via React's useId or a small
helper) and set that id as the label's htmlFor (or aria-labelledby) and pass the
same id into the Datepicker's input prop (or id/inputId/inputProps.id depending
on Datepicker API) so the label and input are programmatically associated;
update both places where you render the date label + <Datepicker> (the Text
label and the corresponding Datepicker component) to use the shared id and, if
Datepicker only exposes an inputRef, set aria-labelledby on the input to the
label id.
- Around line 110-115: The Save button no longer prevents clicks while an async
save is in progress and _onSubmit lacks a loading short-circuit, which allows
duplicate submissions; add an early return in _onSubmit like `if (loading)
return` to short-circuit when a save is in progress, and/or restore the loading
check in the Button's disabled prop (ensure the Button rendering that uses props
size='lg' onClick={_onSubmit} includes `disabled={!date || !text.trim() ||
!allowedToManage || loading}`); apply the same guard pattern to the other
save/button instance that uses the same _onSubmit handler.

In `@web/app/pages/ErrorTracking/index.tsx`:
- Around line 157-160: The current map rendering uses the localized string as
React key (key={item}) which can collide; in the mapping inside ErrorTracking's
render where _map(t('errors.fast.list', { returnObjects: true }), (item) => ...)
replace the key with a stable unique value (e.g., include the map index or an
existing id) by changing the iterator to (item, idx) and using a key like
`error-fast-${idx}` or item.id if items are objects, and keep the Text component
and content identical.

In `@web/app/pages/Organisations/Settings/People.tsx`:
- Around line 173-180: The remove action button (Button with onClick={() =>
onRemove(member)}) is icon-only and lacks an accessible name; add an aria-label
to the Button (for example aria-label={`Remove ${member.name || 'member'}`}) so
screen readers announce the action, and mark the TrashIcon decorative by passing
aria-hidden={true} (or role="img" aria-hidden) to the TrashIcon component so it
isn't announced twice.

In `@web/app/routes/tools.meta-tags-generator.tsx`:
- Around line 276-283: The rendering uses new URL(...) directly on data.url
which can throw for malformed values and crash the page; update the hostname
extraction (where new URL(...) is invoked for rendering the Text paragraph) to
safely parse the input—either wrap the URL construction in a try/catch or add a
small helper like getHostnameFromUrl(url) that returns a default ('example.com'
or '') on error or when url is falsy; replace the inline new URL(...) expression
in the component with a call to that safe parser so invalid input no longer
throws during render.

In `@web/app/ui/MultiSelect.tsx`:
- Around line 139-143: Replace usage of itemKey(items[activeIndex]) when
building DOM ids with a deterministic index-based id: construct activeOptionId
(and the option id generation used elsewhere around the option rendering logic)
from optionIdPrefix + the item's index (e.g., `${optionIdPrefix}-${index}`)
instead of itemKey(item) so ids are valid, unique and stable for
aria-activedescendant; update any references to option ids in the same component
to use this index-based scheme and ensure activeIndex is used to derive the id
consistently.

In `@web/app/ui/ProgressRing.tsx`:
- Around line 86-99: The computed displayValue and ARIA attributes use
normalizedValue directly and can become NaN/Infinity; update the logic around
normalizedValue and displayValue in ProgressRing.tsx to guard non-finite inputs
(use Number.isFinite(normalizedValue) ? Math.floor(normalizedValue) : 0) and
then use that safe displayValue for aria-valuenow and aria-label (or omit
aria-valuenow when not finite) so the component never renders "NaN%" or an
invalid ARIA value; modify the code that defines displayValue and the JSX
attributes referencing it accordingly (look for normalizedValue, displayValue,
aria-valuenow, aria-label).

---

Outside diff comments:
In `@web/app/pages/Project/View/components/RefreshStatsButton.tsx`:
- Around line 55-63: The refresh logic can leave isRefreshing stuck true and
produce unhandled rejections if onRefresh throws; wrap the await calls to
onRefresh(false) (manual path inside the IIFE) and the auto-refresh await (the
block around lines using startTimeRef/hasTriggeredRefresh/setProgress) in
try/finally so setIsRefreshing(false), startTimeRef.current reset,
hasTriggeredRefresh.current = false and setProgress(0) always run, and
additionally catch/log errors in the auto path to avoid unhandled promise
rejections; locate the logic around the anonymous async IIFE and the
auto-refresh block that calls onRefresh and apply try/finally (with a catch in
the auto path) to restore UI state on failure.

In `@web/app/root.tsx`:
- Around line 255-275: The crash stack toggle button (using setCrashStackShown
and crashStackShown) lacks ARIA disclosure attributes—add
aria-expanded={crashStackShown} to the button and give the stack container (the
span rendering error.stack) a stable id (e.g., crash-stack) and add
aria-controls pointing to that id on the button; ensure the controlled span has
role="region" or aria-hidden appropriately so screen readers can detect the
expanded/collapsed state and target.

In `@web/app/routes/_index.tsx`:
- Around line 335-345: The overlay reveal is currently only triggered by hover
via the "group-hover" tailwind classes on the overlay div; update that overlay's
class list (the div containing the CTA anchor and ArrowRightIcon) to also
include the "group-focus-within" variants (e.g., add
group-focus-within:pointer-events-auto and group-focus-within:opacity-100) so
keyboard focus on the live demo link (LIVE_DEMO_URL anchor) will reveal the
overlay as well.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f9c84ac8-7420-43d3-be56-bf3e462e3f99

📥 Commits

Reviewing files that changed from the base of the PR and between b6fe522 and b380dc4.

📒 Files selected for processing (91)
  • web/app/components/ExitIntentPopup.tsx
  • web/app/components/Footer/index.tsx
  • web/app/components/Header/index.tsx
  • web/app/components/marketing/DitchGoogle.tsx
  • web/app/components/marketing/FAQ.tsx
  • web/app/components/pricing/BillingPricing.tsx
  • web/app/components/pricing/MarketingPricing.tsx
  • web/app/modals/AnnotationModal.tsx
  • web/app/modals/ViewProjectHotkeys.tsx
  • web/app/pages/Auth/CreateNewPassword/CreateNewPassword.tsx
  • web/app/pages/Auth/ForgotPassword/ForgotPassword.tsx
  • web/app/pages/Auth/Signup/InvitationSignup.tsx
  • web/app/pages/Blog/Post.tsx
  • web/app/pages/Contact/index.tsx
  • web/app/pages/ErrorTracking/index.tsx
  • web/app/pages/Landings/Marketers.tsx
  • web/app/pages/Landings/SmallBusinesses.tsx
  • web/app/pages/Landings/Startups.tsx
  • web/app/pages/NotFound/index.tsx
  • web/app/pages/OpenStartup/index.tsx
  • web/app/pages/Organisations/Settings/People.tsx
  • web/app/pages/Organisations/Settings/Projects.tsx
  • web/app/pages/Organisations/Settings/index.tsx
  • web/app/pages/Performance/index.tsx
  • web/app/pages/Project/Settings/Alerts/ProjectAlertsSettings.tsx
  • web/app/pages/Project/Settings/Alerts/ProjectAlertsView.tsx
  • web/app/pages/Project/Settings/Annotations.tsx
  • web/app/pages/Project/Settings/Emails/Emails.tsx
  • web/app/pages/Project/Settings/People/People.tsx
  • web/app/pages/Project/Settings/ProjectSettings.tsx
  • web/app/pages/Project/Settings/tabs/AccessSettings.tsx
  • web/app/pages/Project/Settings/tabs/DangerZone.tsx
  • web/app/pages/Project/Settings/tabs/General.tsx
  • web/app/pages/Project/Settings/tabs/Shields.tsx
  • web/app/pages/Project/View/components/AddAViewModal.tsx
  • web/app/pages/Project/View/components/LiveVisitorsDropdown.tsx
  • web/app/pages/Project/View/components/PasswordRequiredModal.tsx
  • web/app/pages/Project/View/components/RefreshStatsButton.tsx
  • web/app/pages/Project/View/components/SearchFilters.tsx
  • web/app/pages/Project/tabs/AskAI/AIChart.tsx
  • web/app/pages/Project/tabs/AskAI/AskAIView.tsx
  • web/app/pages/Project/tabs/Errors/ErrorDetails.tsx
  • web/app/pages/Project/tabs/Errors/NoErrorDetails.tsx
  • web/app/pages/Project/tabs/Experiments/ExperimentSettingsModal.tsx
  • web/app/pages/Project/tabs/Experiments/ExperimentsView.tsx
  • web/app/pages/Project/tabs/FeatureFlags/FeatureFlagsView.tsx
  • web/app/pages/Project/tabs/Funnels/FunnelsView.tsx
  • web/app/pages/Project/tabs/Goals/GoalSettingsModal.tsx
  • web/app/pages/Project/tabs/Goals/GoalsView.tsx
  • web/app/pages/Project/tabs/Sessions/NoSessionDetails.tsx
  • web/app/pages/Project/tabs/Sessions/SessionDetailView.tsx
  • web/app/pages/Project/tabs/Traffic/TrafficView.tsx
  • web/app/pages/Project/tabs/Traffic/UserFlow.tsx
  • web/app/pages/UserSettings/UserSettings.tsx
  • web/app/pages/UserSettings/components/Organisations.tsx
  • web/app/pages/UserSettings/components/ProjectList.tsx
  • web/app/pages/UserSettings/components/TwoFA.tsx
  • web/app/root.tsx
  • web/app/routes/_index.tsx
  • web/app/routes/blog._index.tsx
  • web/app/routes/google-analytics-alternative.tsx
  • web/app/routes/pay.tsx
  • web/app/routes/tools.ab-test-calculator.tsx
  • web/app/routes/tools.ad-cost-calculator.tsx
  • web/app/routes/tools.conversion-rate-calculator.tsx
  • web/app/routes/tools.ctr-calculator.tsx
  • web/app/routes/tools.ltv-calculator.tsx
  • web/app/routes/tools.meta-tags-generator.tsx
  • web/app/routes/tools.og-preview.tsx
  • web/app/routes/tools.percentage-calculator.tsx
  • web/app/routes/tools.qr-code-generator.tsx
  • web/app/routes/tools.robots-txt-generator.tsx
  • web/app/routes/tools.roi-calculator.tsx
  • web/app/routes/tools.schema-markup-generator.tsx
  • web/app/routes/tools.utm-generator.tsx
  • web/app/ui/Combobox.tsx
  • web/app/ui/FileUpload.tsx
  • web/app/ui/Input.tsx
  • web/app/ui/MultiProgress.tsx
  • web/app/ui/MultiSelect.tsx
  • web/app/ui/Pagination.tsx
  • web/app/ui/PasswordStrength.tsx
  • web/app/ui/ProgressRing.tsx
  • web/app/ui/StatusPage.tsx
  • web/app/ui/Switch.tsx
  • web/app/ui/Text.tsx
  • web/public/locales/de.json
  • web/public/locales/en.json
  • web/public/locales/fr.json
  • web/public/locales/pl.json
  • web/public/locales/uk.json
✅ Files skipped from review due to trivial changes (11)
  • web/public/locales/en.json
  • web/app/pages/Project/Settings/tabs/General.tsx
  • web/public/locales/pl.json
  • web/public/locales/uk.json
  • web/app/pages/Project/Settings/tabs/Shields.tsx
  • web/public/locales/de.json
  • web/public/locales/fr.json
  • web/app/pages/Project/View/components/AddAViewModal.tsx
  • web/app/components/Header/index.tsx
  • web/app/pages/Project/Settings/tabs/AccessSettings.tsx
  • web/app/routes/tools.utm-generator.tsx
🚧 Files skipped from review as they are similar to previous changes (20)
  • web/app/pages/Auth/CreateNewPassword/CreateNewPassword.tsx
  • web/app/pages/Project/tabs/Goals/GoalSettingsModal.tsx
  • web/app/pages/Auth/ForgotPassword/ForgotPassword.tsx
  • web/app/pages/Project/tabs/Errors/ErrorDetails.tsx
  • web/app/pages/Project/View/components/PasswordRequiredModal.tsx
  • web/app/pages/Organisations/Settings/index.tsx
  • web/app/pages/Project/tabs/Funnels/FunnelsView.tsx
  • web/app/components/pricing/BillingPricing.tsx
  • web/app/ui/Text.tsx
  • web/app/pages/Project/Settings/People/People.tsx
  • web/app/routes/tools.percentage-calculator.tsx
  • web/app/pages/Project/Settings/Alerts/ProjectAlertsView.tsx
  • web/app/pages/Organisations/Settings/Projects.tsx
  • web/app/ui/StatusPage.tsx
  • web/app/ui/Input.tsx
  • web/app/ui/Pagination.tsx
  • web/app/pages/Project/tabs/Experiments/ExperimentSettingsModal.tsx
  • web/app/pages/Project/tabs/AskAI/AskAIView.tsx
  • web/app/ui/Combobox.tsx
  • web/app/routes/tools.conversion-rate-calculator.tsx

Comment thread web/app/modals/AnnotationModal.tsx Resolved
Comment thread web/app/modals/AnnotationModal.tsx Resolved
Comment thread web/app/pages/ErrorTracking/index.tsx Resolved
Comment thread web/app/ui/MultiSelect.tsx Resolved
Comment thread web/app/ui/ProgressRing.tsx Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
web/app/ui/ProgressRing.tsx (1)

63-72: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Non-finite value still propagates into ring math.

At Line 64, Math.min/Math.max does not sanitize NaN, so normalizedValue can still become NaN. That reaches Line 71 and produces an invalid strokeDashoffset, breaking visual progress rendering despite the later displayValue guard.

Proposed fix
-  const normalizedValue = useMemo(
-    () => Math.min(100, Math.max(0, value)),
-    [value],
-  )
+  const normalizedValue = useMemo(() => {
+    if (!Number.isFinite(value)) return 0
+    return Math.min(100, Math.max(0, value))
+  }, [value])
#!/bin/bash
# Verify where normalizedValue is computed and where it flows into rendering math/ARIA.
rg -n -C2 'normalizedValue|strokeDashoffset|aria-valuenow|displayValue' web/app/ui/ProgressRing.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/ui/ProgressRing.tsx` around lines 63 - 72, normalizedValue can become
NaN because Math.min/Math.max don't guard non-finite inputs; update the useMemo
that computes normalizedValue to first coerce/sanitize value (use
Number.isFinite or similar) into a finite number (e.g., default to 0 or clamp to
bounds) and then apply Math.max/Math.min, so stroke math in the subsequent
useMemo (radius, circumference, strokeDashoffset) always sees a finite numeric
normalizedValue; ensure the dependency array still references value and that
displayValue/aria-valuenow logic continues to use the sanitized normalizedValue.
🧹 Nitpick comments (1)
web/app/pages/Organisations/Settings/People.tsx (1)

429-435: Remove unsafe role cast by narrowing invite form role type.

On Line 433, (form.role as 'admin' | 'viewer') casts a Role type (which includes 'owner') to a narrower union. This bypasses type safety—if the invite form ever accidentally assigns 'owner' to form.role, the cast silently allows it through. Create a dedicated InviteRole = 'admin' | 'viewer' type and use it for the form state instead:

+type InviteRole = 'admin' | 'viewer'
+
 const [form, setForm] = useState<{
   email: string
-  role: Role
+  role: InviteRole
 }>({
   email: '',
   role: 'viewer',
 })
@@
-          <RadioCardGroup<'admin' | 'viewer'>
+          <RadioCardGroup<InviteRole>
             name='role'
             label={t('project.settings.role')}
             className='mt-4'
-            value={(form.role as 'admin' | 'viewer') || null}
+            value={form.role}
             onChange={(role) => setForm((prev) => ({ ...prev, role }))}
             error={errors.role}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/app/pages/Organisations/Settings/People.tsx` around lines 429 - 435, The
code unsafely casts form.role to 'admin'|'viewer' for RadioCardGroup; define a
new InviteRole = 'admin' | 'viewer' type and change the invite form state so its
role property is InviteRole (replace usages of the broader Role type for the
invite form), then remove the cast in RadioCardGroup value and ensure onChange
calls setForm((prev) => ({ ...prev, role })) use the narrowed InviteRole type;
update any helpers/initial state that construct the invite form to use
InviteRole so assigning 'owner' becomes a type error instead of a silent cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/app/pages/Project/View/components/RefreshStatsButton.tsx`:
- Around line 84-88: The manual refresh path in handleManualRefresh currently
awaits onRefresh(true) inside a try/finally but does not catch errors, causing
unhandled promise rejections; update handleManualRefresh to mirror the
auto-refresh logic by wrapping the await onRefresh(true) in a try/catch/finally
(or add a .catch) and log errors using the same logger used by the auto-refresh
path (e.g., processLogger or console.error) before calling
setIsRefreshing(false) in the finally block so errors are handled and state is
always reset.

In `@web/app/root.tsx`:
- Around line 275-279: The region element with id 'crash-stack' (role='region')
is unlabeled for assistive tech; add an accessible name by adding either an
aria-label (e.g., aria-label="Crash stack trace" or similar) or aria-labelledby
pointing to a nearby heading element that describes the content, ensuring the
label is concise and descriptive and updating any related heading id if using
aria-labelledby.

---

Duplicate comments:
In `@web/app/ui/ProgressRing.tsx`:
- Around line 63-72: normalizedValue can become NaN because Math.min/Math.max
don't guard non-finite inputs; update the useMemo that computes normalizedValue
to first coerce/sanitize value (use Number.isFinite or similar) into a finite
number (e.g., default to 0 or clamp to bounds) and then apply Math.max/Math.min,
so stroke math in the subsequent useMemo (radius, circumference,
strokeDashoffset) always sees a finite numeric normalizedValue; ensure the
dependency array still references value and that displayValue/aria-valuenow
logic continues to use the sanitized normalizedValue.

---

Nitpick comments:
In `@web/app/pages/Organisations/Settings/People.tsx`:
- Around line 429-435: The code unsafely casts form.role to 'admin'|'viewer' for
RadioCardGroup; define a new InviteRole = 'admin' | 'viewer' type and change the
invite form state so its role property is InviteRole (replace usages of the
broader Role type for the invite form), then remove the cast in RadioCardGroup
value and ensure onChange calls setForm((prev) => ({ ...prev, role })) use the
narrowed InviteRole type; update any helpers/initial state that construct the
invite form to use InviteRole so assigning 'owner' becomes a type error instead
of a silent cast.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bc736d4-8316-4b86-b748-f91478dbdca8

📥 Commits

Reviewing files that changed from the base of the PR and between b380dc4 and d3244d9.

📒 Files selected for processing (10)
  • web/app/modals/AnnotationModal.tsx
  • web/app/pages/ErrorTracking/index.tsx
  • web/app/pages/Organisations/Settings/People.tsx
  • web/app/pages/Project/View/components/RefreshStatsButton.tsx
  • web/app/root.tsx
  • web/app/routes/_index.tsx
  • web/app/routes/tools.meta-tags-generator.tsx
  • web/app/ui/Datepicker.tsx
  • web/app/ui/MultiSelect.tsx
  • web/app/ui/ProgressRing.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • web/app/routes/_index.tsx
  • web/app/ui/MultiSelect.tsx
  • web/app/pages/ErrorTracking/index.tsx

Comment thread web/app/root.tsx Resolved
@Blaumaus
Blaumaus merged commit e2920f7 into main Apr 30, 2026
9 of 10 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request May 26, 2026
6 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Jun 13, 2026
3 of 6 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.

1 participant