Skip to content

feat(theme): add AMOLED-Strix theme with semantic CSS token system#2697

Closed
RAFSuNX wants to merge 8 commits intoseerr-team:developfrom
RAFSuNX:feat/theming-system
Closed

feat(theme): add AMOLED-Strix theme with semantic CSS token system#2697
RAFSuNX wants to merge 8 commits intoseerr-team:developfrom
RAFSuNX:feat/theming-system

Conversation

@RAFSuNX
Copy link
Copy Markdown

@RAFSuNX RAFSuNX commented Mar 14, 2026

AI Assistance Disclosure

This PR was developed with Claude Code (Anthropic) as a coding assistant. All code was reviewed by me and tested against a live Seerr instance running both the default and AMOLED-Strix themes. I understand the implementation and can answer questions about it.


Summary

This PR introduces an AMOLED-Strix theme variant and a semantic CSS token system that makes all theme-aware components fully declarative — no per-component theme logic required.

What's included

1. Theme system foundation (src/styles/globals.css, tailwind.config.js)**

A new [data-theme='amoled-strix'] attribute on <html> remaps 7 semantic color tokens:

Token Default (gray) AMOLED-Strix
--color-bg 17 24 39 0 0 0
--color-surface 31 41 55 14 14 14
--color-surface-raised 55 65 81 24 24 24
--color-input 31 41 55 10 10 10
--color-border 55 65 81 36 36 36
--color-border-muted 52 52 52 52 52 52
--color-ring 55 65 81 255 255 255

Tokens use bare RGB triplets (not var() references) so Tailwind opacity modifiers like bg-surface/80 work correctly.

These wire into Tailwind as: bg-app, bg-surface, bg-surface-raised, bg-input, border-border-default, border-border-muted, ring-ring-app.

2. Appearance settings UI

A new "Appearance" settings page lets users switch between Default and AMOLED-Strix themes. The selection is persisted via a data-theme attribute on <html> and stored in user preferences.

3. AMOLED-Strix visual design

The AMOLED-Strix theme includes:

  • Pure black page background with near-black (14,14,14) card surfaces — cards are visible but don't wash into the background
  • Cinematic full-bleed hero on movie/TV detail pages
  • Redesigned title cards with glass badge, stronger gradient, and glass request button
  • Always-expanded search box on desktop (collapsible on mobile)
  • Custom AMOLED navbar (transparent, floating)
  • Hero slider on the Discover page
  • Redesigned slider section headers and chevron buttons
  • Blurred backdrop image on Discover pages

4. Semantic token migration

All isAmoled component-level conditionals are replaced with semantic classes. Two approaches are used:

  • Color swaps (isAmoled ? 'bg-black' : 'bg-gray-800'bg-surface): handled via Tailwind semantic classes backed by CSS custom properties
  • Structural changes (conditional JSX, border-radius, layout offsets): moved to [data-theme='amoled-strix'] .stable-class { ... } rules in globals.css

After migration:

  • isAmoled exists in exactly one file: ImageFader/index.tsx — a planned exception where the value is used to compute a CSS gradient string passed as an inline style (cannot be expressed as a class)
  • useTheme is retained only in ImageFader and the theme switcher UI (UserAppearanceSettings)

Test plan

  • Default theme: cards have dark gray background, inputs slightly lighter, dropdowns elevated above cards, table headers distinct from rows
  • AMOLED-Strix: page is pure black, cards visible with subtle white ring, inputs slightly darker, no elements invisible against background
  • Switching themes in Appearance settings takes effect immediately without page reload
  • Movie/TV detail pages: cinematic hero displays correctly in both themes
  • Discover page: hero slider appears in AMOLED-Strix, standard layout in default
  • Production build passes with no TypeScript or lint errors

Files changed

  • src/styles/globals.css — semantic token definitions + structural AMOLED CSS selectors
  • tailwind.config.js — semantic color entries
  • src/components/UserProfile/UserSettings/UserAppearanceSettings/ — new appearance settings page
  • src/components/Layout/AmoledNavbar/ — new AMOLED navbar
  • src/components/Discover/HeroSlider/ — new hero slider component
  • ~55 existing components — isAmoled conditionals replaced with semantic tokens

Summary by CodeRabbit

  • New Features

    • Theme customization with Default and AMOLED themes, global theme support, Appearance settings pages (user & admin).
    • AMOLED UX additions: new AMOLED navbar, AMOLED full‑bleed layouts for Movie/TV, and an auto‑advancing Hero carousel on Discover.
    • Title cards: optional user score display and updated request/watch controls.
  • Style

    • Broad visual refresh: surface tokens, new gradients/overlays, blur/vignette, AMOLED-specific styling and scrollbar theming.
    • New "glass" button/dropdown variant; tightened no-wrap text for buttons and dropdowns.
  • Accessibility

    • Improved slider navigation with aria labels.

@RAFSuNX RAFSuNX requested a review from a team as a code owner March 14, 2026 14:04
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a theme system (including an AMOLED "amoled-strix" theme), ThemeProvider/useTheme, appearance settings/pages, a new AmoledNavbar and HeroSlider, a "glass" button variant, many theme-aware styling/token swaps, gradient overlay refactors, and multiple AMOLED-specific rendering paths across detail pages.

Changes

Cohort / File(s) Summary
Theme system & integration
src/context/ThemeContext.tsx, src/hooks/useTheme.ts, src/pages/_app.tsx, src/pages/_document.tsx
Adds ThemeContext, THEMES, ThemeProvider, useTheme hook, document bootstrap for data-theme, and wraps the app with ThemeProvider.
Global styles & Tailwind tokens
src/styles/globals.css, tailwind.config.js
Adds CSS variables and AMOLED overrides, scrollbar/sidebar rules, and extends Tailwind with semantic color tokens (surface, input, border, ring, app, gray/indigo scales).
Appearance UI & pages
src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx, src/components/UserProfile/UserSettings/index.tsx, src/pages/profile/settings/appearance.tsx, src/pages/users/[userId]/settings/appearance.tsx
Adds UserAppearanceSettings component, adds Appearance route to settings menu, and pages for user and admin to change theme.
AMOLED navbar & layout integration
src/components/Layout/AmoledNavbar/index.tsx, src/components/Layout/index.tsx, src/components/Layout/PullToRefresh/index.tsx, src/components/Layout/UserDropdown/index.tsx
Introduces AmoledNavbar, integrates it into layout, and applies small token swaps in pull-to-refresh and user dropdown.
HeroSlider & Discover integration
src/components/Discover/HeroSlider/index.tsx, src/components/Discover/index.tsx, src/components/Discover/DiscoverMovies/index.tsx, src/components/Discover/DiscoverTv/index.tsx
Adds HeroSlider (trending carousel) and conditionally renders it for AMOLED; DiscoverMovies/DiscoverTv add amoled-specific backdrop selection and layout adjustments.
Large AMOLED detail pages & TitleCard
src/components/MovieDetails/index.tsx, src/components/TvDetails/index.tsx, src/components/TitleCard/index.tsx
Adds alternate AMOLED full-bleed rendering paths, cinematic hero layouts, new badges/controls, and significant DOM/layout changes while preserving data flows.
Button, dropdown & glass variant
src/components/Common/ButtonWithDropdown/index.tsx, src/components/Common/Dropdown/index.tsx, src/components/Common/PlayButton/index.tsx, src/components/RequestButton/index.tsx
Introduces 'glass' buttonType across ButtonWithDropdown and Dropdown, updates PlayButton/RequestButton to use glass styling, and prevents text wrapping.
Image/gradient & theme-aware components
src/components/Common/ImageFader/index.tsx, src/components/PersonDetails/index.tsx, src/components/MediaSlider/index.tsx
Makes gradient logic theme-aware (isAmoled), replaces inline gradients with Tailwind overlay divs, and refactors media slider header with i18n.
Widespread semantic token swaps & overlays
src/components/... (many small files, e.g., Common/*, RequestCard/*, Issue*/*, Settings/*, Login/*, RequestList/*, Toast, Table, Tag, Tooltip)
Replaces many gray-* tokens with semantic tokens (bg-surface, bg-input, border-border-default, ring-ring-app), replaces inline gradient styles with overlay elements, and adjusts class composition across many components.
Accessibility, keys & small fixes
src/components/Settings/OverrideRule/OverrideRuleTiles.tsx, src/components/Slider/index.tsx, src/components/RequestButton/index.tsx
Adds missing React keys, aria-labels for slider nav, prevents button text wrapping, and minor structural/UI tweaks.
New components/pages & wiring
src/components/Layout/AmoledNavbar/index.tsx, src/components/Discover/HeroSlider/index.tsx, src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx, src/pages/profile/settings/appearance.tsx, src/pages/users/[userId]/settings/appearance.tsx
Introduces several new components/pages and wires them into layout and routing.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant App as App UI
  participant ThemeProv as ThemeProvider
  participant DOM as Document (data-theme)
  participant Comp as Theme-aware Components

  User->>App: open Appearance settings
  App->>ThemeProv: request current theme
  ThemeProv-->>App: theme + setTheme()
  User->>App: selects theme
  App->>ThemeProv: call setTheme(themeId)
  ThemeProv->>DOM: set data-theme attribute
  ThemeProv-->>App: context updated
  App->>Comp: re-render (useTheme)
  Comp-->>App: render AMOLED variants/backdrops
Loading
sequenceDiagram
  participant App as App UI
  participant Hero as HeroSlider
  participant API as /api/v1/discover/trending
  participant Cache as CachedImage
  participant Modal as RequestModal

  App->>Hero: mount
  Hero->>API: fetch trending (SWR)
  API-->>Hero: trending items
  Hero->>Cache: preload backdrops
  Hero-->>App: display rotating slides
  User->>Hero: click Request
  Hero->>Modal: open RequestModal
  Modal-->>Hero: onComplete (update status)
  Hero->>App: update action state
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • gauthier-th
  • 0xSysR3ll
  • M0NsTeRRR

Poem

"I hopped through CSS and tokens bright,
I nudged the theme from gray to night,
I stitched a navbar, slider, glow,
Replaced gradients row by row,
The rabbit cheers: the UI took flight." 🐇✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: adding an AMOLED-Strix theme with semantic CSS token system. It is concise, specific, and clearly conveys the primary feature being introduced.

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

📝 Coding Plan
  • Generate coding plan for human review comments

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

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

@RAFSuNX RAFSuNX force-pushed the feat/theming-system branch from e1b55e6 to 56a92b0 Compare March 14, 2026 14:07
@RAFSuNX
Copy link
Copy Markdown
Author

RAFSuNX commented Mar 14, 2026

Hey! I'm not a regular contributor and this is actually my first PR to the project, so apologies if anything isn't quite right. I just really wanted an AMOLED theme option for use on OLED screens - the default UI looks great, I have no complaints there, I just wanted a pure black variant to take advantage of the display. Thought it might be useful for others too, so figured I'd share it rather than keep it to myself. Hopefully the devs get a chance to look it over. Happy to make any changes if anything needs adjusting!

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

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

⚠️ Outside diff range comments (2)
src/components/Login/JellyfinLogin.tsx (1)

124-130: ⚠️ Potential issue | 🔴 Critical

Restore the ! flag on these background utilities.

The input[type='text'] and input[type='password'] selectors in globals.css (line 377–381) apply bg-gray-700 with @apply. This attribute selector has higher specificity than the plain bg-input class, so bg-gray-700 will override it at runtime. The codebase elsewhere (e.g., RequestList.tsx) already uses !bg-input for this exact reason.

Required fix
-                        className="bg-input placeholder:text-gray-400"
+                        className="!bg-input placeholder:text-gray-400"

Apply the same change to the password field at line ~152.

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

In `@src/components/Login/JellyfinLogin.tsx` around lines 124 - 130, The
background utility on the input Fields is being overridden by the
input[type='text'] / input[type='password'] selector, so restore the important
flag by changing the className on the Field with id="username" (and the
corresponding Field with id="password") in the JellyfinLogin component to use
"!bg-input" instead of "bg-input" (keep the other classes like
"placeholder:text-gray-400"); this mirrors the existing fix used in
RequestList.tsx and ensures the intended background class wins specificity.
src/pages/_app.tsx (1)

203-223: ⚠️ Potential issue | 🟡 Minor

Set the initial theme during SSR or via an inline script to prevent a flash for users with persisted preferences.

The ThemeProvider applies data-theme inside a useEffect, which runs after hydration. Since _document.tsx does not set an initial data-theme attribute and no inline script runs before paint, users with the 'amoled-strix' preference will see the default theme briefly when refreshing before the effect sets the correct theme. Add an inline script in _document.tsx that reads localStorage and sets document.documentElement.setAttribute('data-theme', ...) before the page renders, or pass the theme from the server during SSR.

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

In `@src/pages/_app.tsx` around lines 203 - 223, The app flashes the wrong theme
because ThemeProvider only sets the data-theme inside a client-side useEffect;
to fix, add an inline script in the custom document that runs before paint and
sets document.documentElement.setAttribute('data-theme', <value>) by reading the
same localStorage key your ThemeProvider uses (or alternatively pass the
persisted theme from SSR into currentSettings and render it on the server);
target the ThemeProvider/data-theme behavior and ensure the inline script reads
the identical storage key and sets the attribute synchronously so the correct
theme is present on first paint.
🧹 Nitpick comments (18)
src/components/IssueModal/CreateIssueModal/index.tsx (1)

247-247: Use a plain string literal for className here.

Nice token migration to bg-surface/50. Since there’s no interpolation, this can be a normal string to reduce noise.

♻️ Suggested cleanup
- <div className={`-space-y-px overflow-hidden rounded-md bg-surface/50`}>
+ <div className="-space-y-px overflow-hidden rounded-md bg-surface/50">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/IssueModal/CreateIssueModal/index.tsx` at line 247, In the
CreateIssueModal component, replace the unnecessary template literal used for
the div's className (currently written as className={`-space-y-px
overflow-hidden rounded-md bg-surface/50`}) with a plain string literal "
-space-y-px overflow-hidden rounded-md bg-surface/50" (i.e.,
className="-space-y-px overflow-hidden rounded-md bg-surface/50") since there is
no interpolation; locate the JSX div with that className and update it
accordingly to reduce noise.
src/components/Discover/Trending.tsx (1)

55-57: Migration to semantic tokens looks correct.

Both icon-wrapper spans are now using the semantic border-border-default and bg-input tokens, aligning with the PR's theme token migration.

Minor nit: the template literals (backticks) aren't needed since there's no interpolation—a plain string would suffice.

,

♻️ Optional: Use plain strings instead of template literals
-            <span className={`inline-flex cursor-default items-center rounded-l-md border border-r-0 px-3 text-sm text-gray-100 border-border-default bg-input`}>
+            <span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 px-3 text-sm text-gray-100 border-border-default bg-input">

Apply similar change to line 77.

Also applies to: 77-79

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

In `@src/components/Discover/Trending.tsx` around lines 55 - 57, In the Trending
component, replace unnecessary template literal className values with plain
strings for the icon-wrapper spans (the span containing CircleStackIcon and the
other span around the second icon at lines ~77-79) — update
className={`inline-flex cursor-default items-center rounded-l-md border
border-r-0 px-3 text-sm text-gray-100 border-border-default bg-input`} and the
similar occurrence to simply use "inline-flex cursor-default items-center
rounded-l-md border border-r-0 px-3 text-sm text-gray-100 border-border-default
bg-input" (i.e., remove backticks and curly braces) so there is no template
literal when no interpolation is present.
src/components/Layout/PullToRefresh/index.tsx (1)

120-120: Tokenize border/ring here for full theme consistency.

bg-surface-raised is migrated, but border-gray-800 and ring-gray-700 remain hardcoded.

Proposed class update
-        } relative -top-28 h-9 w-9 rounded-full border-4 shadow-md shadow-black ring-1 border-gray-800 bg-surface-raised ring-gray-700`}
+        } relative -top-28 h-9 w-9 rounded-full border-4 shadow-md shadow-black ring-1 border-border-default bg-surface-raised ring-ring-app/20`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Layout/PullToRefresh/index.tsx` at line 120, The JSX in the
PullToRefresh component (the element whose className contains "relative -top-28
h-9 w-9 rounded-full border-4 shadow-md shadow-black ring-1 border-gray-800
bg-surface-raised ring-gray-700") uses hardcoded color classes; replace
border-gray-800 and ring-gray-700 with your theme token classes (e.g., the
project’s tokenized border and ring utilities such as border-surface-stroke and
ring-surface-outline or the equivalent token names in your design system) so the
component uses tokenized colors consistently with bg-surface-raised; update the
className string in the PullToRefresh render (the template literal containing
those classes) to use the token names.
src/components/Common/Modal/index.tsx (1)

119-119: Finish semantic token migration in modal chrome/backdrop.

The modal container ring and backdrop gradient still use fixed grays (ring-gray-700, from-gray-800/75, to-gray-800), which weakens theme fidelity for AMOLED-Strix.

Proposed tweak
-className={`hide-scrollbar relative inline-block w-full overflow-auto px-4 pb-4 pt-4 text-left align-bottom shadow-xl ring-1 transition-all sm:my-8 sm:max-w-3xl sm:rounded-lg sm:align-middle bg-surface ring-gray-700 ${dialogClass}`}
+className={`hide-scrollbar relative inline-block w-full overflow-auto px-4 pb-4 pt-4 text-left align-bottom bg-surface shadow-xl ring-1 ring-ring-app/10 transition-all sm:my-8 sm:max-w-3xl sm:rounded-lg sm:align-middle ${dialogClass}`}

-<div className={`absolute inset-0 bg-gradient-to-b from-gray-800/75 to-gray-800`} />
+<div className="absolute inset-0 bg-gradient-to-b from-surface-raised/75 to-surface" />

Also applies to: 147-147

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

In `@src/components/Common/Modal/index.tsx` at line 119, The modal component still
uses hardcoded gray tokens in the container and backdrop (see the className on
the modal container that includes "ring-gray-700" and the backdrop gradient
classes referencing "from-gray-800/75" and "to-gray-800"); replace those fixed
color classes with the semantic token equivalents used by the theme (e.g., use
the project's semantic ring/backdrop tokens instead of ring-gray-700,
from-gray-800/75, to-gray-800) so the modal respects AMOLED-Strix theming—update
the className string on the modal container and the backdrop gradient
construction (references: dialogClass and the modal backdrop element) to use the
appropriate semantic token classes.
src/components/Common/SlideOver/index.tsx (1)

74-74: Use semantic ring token for theme consistency.

bg-surface is migrated, but ring-gray-700 is still hard-coded and can look off under AMOLED-Strix. Prefer the semantic ring token here too.

Proposed tweak
-<div className={`flex h-full flex-col rounded-lg shadow-xl ring-1 backdrop-blur bg-surface ring-gray-700`}>
+<div className="flex h-full flex-col rounded-lg bg-surface shadow-xl ring-1 ring-ring-app/10 backdrop-blur">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Common/SlideOver/index.tsx` at line 74, The hard-coded color
token "ring-gray-700" in the SlideOver component's container div should be
replaced with your design system's semantic ring token to ensure theme
consistency; update the className on the div in
src/components/Common/SlideOver/index.tsx (the line containing the JSX: div
className={`flex h-full flex-col rounded-lg shadow-xl ring-1 backdrop-blur
bg-surface ring-gray-700`}) to use the semantic token (for example replace
ring-gray-700 with your semantic token such as ring-border or ring-ring) so the
ring color follows theming across themes.
src/components/PersonDetails/index.tsx (1)

245-266: Deduplicate backdrop image derivation for both faders.

The same filter/map/slice pipeline is repeated twice. Extract it once (memoized) and reuse it in both ImageFader instances.

♻️ Suggested refactor
+  const backdropImages = useMemo(
+    () =>
+      [...(sortedCast ?? []), ...(sortedCrew ?? [])]
+        .filter((media) => media.backdropPath)
+        .map(
+          (media) =>
+            `https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${media.backdropPath}`
+        )
+        .slice(0, 6),
+    [sortedCast, sortedCrew]
+  );
...
-      {(sortedCrew || sortedCast) && (
+      {backdropImages.length > 0 && (
         <>
           <div className="person-fader-standard absolute left-0 right-0 top-0 z-0 h-96">
-            <ImageFader
-              isDarker
-              backgroundImages={[...(sortedCast ?? []), ...(sortedCrew ?? [])]
-                .filter((media) => media.backdropPath)
-                .map(
-                  (media) =>
-                    `https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${media.backdropPath}`
-                )
-                .slice(0, 6)}
-            />
+            <ImageFader isDarker backgroundImages={backdropImages} />
           </div>
           <div className="person-fader-amoled absolute left-0 right-0 top-0 z-0 h-96 overflow-hidden">
-            <ImageFader
-              isDarker
-              backgroundImages={[...(sortedCast ?? []), ...(sortedCrew ?? [])]
-                .filter((media) => media.backdropPath)
-                .map(
-                  (media) =>
-                    `https://image.tmdb.org/t/p/w1920_and_h800_multi_faces/${media.backdropPath}`
-                )
-                .slice(0, 6)}
-            />
+            <ImageFader isDarker backgroundImages={backdropImages} />
             <div className="absolute inset-0 bg-gradient-to-b from-black/60 to-gray-900" />
           </div>
         </>
       )}

Also applies to: 257-269

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

In `@src/components/PersonDetails/index.tsx` around lines 245 - 266, Extract the
repeated backdrop URL pipeline into a memoized value (e.g., const backdropUrls =
useMemo(() => ..., [sortedCast, sortedCrew])) and replace the two inline arrays
passed to ImageFader with that variable; specifically compute backdropUrls by
concatenating [...(sortedCast ?? []), ...(sortedCrew ?? [])], filtering for
media.backdropPath, mapping to the TMDB URL string, and slicing(0, 6), then pass
backdropUrls to both ImageFader instances to remove duplication while depending
on sortedCast and sortedCrew.
src/components/CollectionDetails/index.tsx (1)

203-214: Let collection-backdrop-overlay own the full gradient.

This element still hard-codes from-gray-900/50 to-gray-900, so the backdrop tint is partly anchored to the default palette even after moving overlay logic into collection-backdrop-overlay. Move the full gradient into the CSS class, or switch these utilities to theme-aware tokens.

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

In `@src/components/CollectionDetails/index.tsx` around lines 203 - 214, The
overlay currently hard-codes Tailwind utility gradient colors in the JSX; update
the JSX in CollectionDetails (the element with className
"collection-backdrop-overlay") to remove the inline utilities "bg-gradient-to-b
from-gray-900/50 to-gray-900" so the element only references the CSS class, then
move the full gradient definition into the stylesheet (or replace the utilities
with theme-aware tokens) for ".collection-backdrop-overlay" so the backdrop tint
is controlled by theme-aware CSS rather than hard-coded palette utilities.
src/components/Common/ImageFader/index.tsx (1)

26-27: Avoid hard-coded theme id strings in component logic.

Comparing against 'amoled-strix' directly can drift from the central theme definitions; prefer a shared ThemeId/THEMES constant.

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

In `@src/components/Common/ImageFader/index.tsx` around lines 26 - 27, Replace the
hard-coded string comparison in ImageFader (const { theme } = useTheme(); const
isAmoled = theme === 'amoled-strix') with a reference to the centralized theme
identifier constant (e.g., ThemeId.AMOLED_STRIX or THEMES.AMOLED_STRIX): import
the appropriate constant from the shared theme definitions and use theme ===
<THEME_CONST> to set isAmoled so the component stays in sync with central theme
enums/IDs.
src/components/Common/Tooltip/index.tsx (1)

28-28: Use semantic border token in tooltip styling.

border-gray-600 bypasses the new theme token system and can drift between themes.

Proposed change
-    `z-50 text-sm absolute font-normal px-2 py-1 rounded border shadow text-gray-100 bg-surface-raised border-gray-600`,
+    `z-50 text-sm absolute font-normal px-2 py-1 rounded border shadow text-gray-100 bg-surface-raised border-border-default`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Common/Tooltip/index.tsx` at line 28, The tooltip CSS
currently uses a hardcoded class "border-gray-600" in the Tooltip component
(src/components/Common/Tooltip/index.tsx) which bypasses theme tokens; update
the class string in the Tooltip (the line that contains `z-50 text-sm absolute
font-normal px-2 py-1 rounded border shadow text-gray-100 bg-surface-raised
border-gray-600`) to use the semantic border token used by the design system
(replace `border-gray-600` with the appropriate theme token, e.g.
`border-surface-border` or `border-border`) so the tooltip border color follows
theme tokens across themes.
src/components/Toast/index.tsx (1)

30-30: Prefer semantic ring token for theme consistency.

ring-gray-500 hard-codes a palette value and weakens AMOLED/default token remapping.

Proposed change
-        <div className={`pointer-events-auto w-full max-w-sm rounded-lg shadow-lg ring-1 bg-surface ring-gray-500`}>
+        <div className={`pointer-events-auto w-full max-w-sm rounded-lg shadow-lg ring-1 bg-surface ring-ring-app`}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Toast/index.tsx` at line 30, In the Toast component replace
the hard-coded palette class "ring-gray-500" on the outer div (the div with
className `pointer-events-auto w-full max-w-sm rounded-lg shadow-lg ring-1
bg-surface ...`) with the semantic ring token used across the design system (for
example "ring-outline" or the project's equivalent), i.e., update the class
string inside the Toast component to use the semantic token instead of
"ring-gray-500" so theme tokens and AMOLED remapping apply consistently.
src/components/RequestCard/index.tsx (1)

53-53: Consider migrating placeholder and error states to semantic tokens.

RequestCardPlaceholder (line 53) uses bg-gray-700 and RequestCardError (line 85) uses bg-gray-800, while the main RequestCard was migrated to bg-surface. For consistent theming, consider updating these as well.

♻️ Suggested changes
// Line 53 - RequestCardPlaceholder
-    <div className="relative w-72 animate-pulse rounded-xl bg-gray-700 p-4 sm:w-96">
+    <div className="relative w-72 animate-pulse rounded-xl bg-surface p-4 sm:w-96">

// Line 85 - RequestCardError (keep ring-red-500 for error indication)
-      className="relative flex w-72 overflow-hidden rounded-xl bg-gray-800 p-4 text-gray-400 shadow ring-1 ring-red-500 sm:w-96"
+      className="relative flex w-72 overflow-hidden rounded-xl bg-surface p-4 text-gray-400 shadow ring-1 ring-red-500 sm:w-96"

Also applies to: 85-85

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

In `@src/components/RequestCard/index.tsx` at line 53, RequestCardPlaceholder and
RequestCardError use hardcoded tailwind classes bg-gray-700 and bg-gray-800;
change them to use the semantic token class used by RequestCard (e.g.,
bg-surface or the project's semantic token like bg-surface-500) so theming is
consistent. Locate the RequestCardPlaceholder component (the div with className
"relative w-72 animate-pulse rounded-xl bg-gray-700 p-4 sm:w-96") and
RequestCardError (the element using "bg-gray-800") and replace those bg-*
classes with the semantic token class used by RequestCard (and update any
pulse/spacing classes unchanged), ensuring both placeholders and error states
follow the same semantic background token.
src/components/Blocklist/index.tsx (1)

116-117: Consider migrating filter controls to semantic tokens for consistency.

The filter and search icon holders (lines 116-117, 147-148) still use hardcoded border-gray-500 bg-gray-800, while similar elements in RequestList were migrated to border-border-default bg-input. For visual consistency across themes, consider updating these as well.

♻️ Suggested change for filter icon holder (line 116)
-            <span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
+            <span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-border-default bg-input px-3 text-sm text-gray-100">

Also applies to: 147-148

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

In `@src/components/Blocklist/index.tsx` around lines 116 - 117, The filter/search
icon holders in src/components/Blocklist (the span wrapping <FunnelIcon> and the
similar span for the search icon) still use hardcoded classes "border-gray-500
bg-gray-800"; update those className values to use the semantic tokens used in
RequestList (e.g., replace "border-gray-500 bg-gray-800" with
"border-border-default bg-input") so the components follow theme tokens and
remain consistent across dark/light themes; ensure you update both occurrences
(the FunnelIcon holder and the search icon holder) in the Blocklist component.
src/components/RequestList/index.tsx (1)

276-276: Consider migrating the hover state to a semantic token.

The hover state hover:!bg-gray-400 still uses a hardcoded gray color while other colors in this element use semantic tokens. For full theme consistency, consider using a semantic hover token if one is available.

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

In `@src/components/RequestList/index.tsx` at line 276, The hover style in the
className on the RequestList component uses a hardcoded color token
("hover:!bg-gray-400"); update that to use the project's semantic token instead
(replace "hover:!bg-gray-400" with the appropriate semantic class/variable used
elsewhere, e.g., "hover:!bg-surface-hover" or a CSS variable like
"hover:!bg-[var(--bg-hover)]") so the element follows theme tokens; edit the
className string in src/components/RequestList/index.tsx where the class
includes "z-40 mr-2 rounded-l-none border border-border-default !bg-input !px-3
!text-gray-500 hover:!bg-gray-400 hover:!text-white" and swap the hardcoded
hover token for the project's semantic hover token.
src/components/Discover/DiscoverTv/index.tsx (1)

70-72: Redundant type assertion.

The titles array is already typed as TvResult[] from useDiscover<TvResult, ...>, so the cast (t as TvResult) on lines 70 and 72 is unnecessary.

♻️ Proposed simplification
-const withBackdrop = titles.filter((t) => (t as TvResult).backdropPath);
+const withBackdrop = titles.filter((t) => t.backdropPath);
 if (!withBackdrop.length) return;
-const pick = withBackdrop[Math.floor(Math.random() * withBackdrop.length)] as TvResult;
+const pick = withBackdrop[Math.floor(Math.random() * withBackdrop.length)];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Discover/DiscoverTv/index.tsx` around lines 70 - 72, The
filter and pick logic redundantly cast each item to TvResult even though titles
is already TvResult[] from useDiscover; remove the unnecessary casts by changing
the filter to const withBackdrop = titles.filter(t => t.backdropPath); and keep
pick as const pick = withBackdrop[Math.floor(Math.random() *
withBackdrop.length)] as TvResult (or drop the cast there too since withBackdrop
is TvResult[]), ensuring you reference the withBackdrop, titles and TvResult
identifiers when making the change.
src/components/Discover/DiscoverMovies/index.tsx (1)

70-77: Consider extracting shared backdrop logic.

The backdrop selection logic here is identical to DiscoverTv. While not critical, extracting this to a shared hook would reduce duplication and ensure consistent behavior.

Example hook signature:

function useRandomBackdrop<T extends { backdropPath?: string }>(items: T[] | undefined): string | null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Discover/DiscoverMovies/index.tsx` around lines 70 - 77, The
backdrop selection useEffect in DiscoverMovies duplicates the same logic used in
DiscoverTv; extract it into a shared hook like useRandomBackdrop<T extends {
backdropPath?: string }>(items: T[] | undefined): string | null and replace the
effect: move filtering for items with backdropPath, random pick, and pickedRef
handling into the hook and return the chosen backdrop; then in this file remove
the useEffect and pickedRef usage and call the hook to get the backdrop value
and pass it to setBackdropPath (or directly use the returned value), keeping
references to useEffect, pickedRef, setBackdropPath, and MovieResult to locate
the original logic and ensure both DiscoverMovies and DiscoverTv consume the new
hook for consistent behavior.
src/components/Discover/HeroSlider/index.tsx (1)

75-80: Potential interval churn on slide changes.

startInterval is recreated when next changes (which depends on currentIndex). Since startInterval is in the dependency array, the interval is cleared and recreated on every slide transition. This is likely benign but could cause subtle timing issues.

♻️ Consider stabilizing with useRef
+const nextRef = useRef(next);
+useEffect(() => { nextRef.current = next; }, [next]);
+
 const startInterval = useCallback(() => {
   if (intervalRef.current) clearInterval(intervalRef.current);
   intervalRef.current = setInterval(() => {
-    if (!isPaused.current) next();
+    if (!isPaused.current) nextRef.current();
   }, ROTATE_INTERVAL);
-}, [next]);
+}, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Discover/HeroSlider/index.tsx` around lines 75 - 80, The
effect is recreating the interval whenever startInterval changes (because
startInterval captures next/currentIndex), causing interval churn; stabilize the
timer by removing the changing callback from the effect deps: either memoize
startInterval with useCallback so it doesn't re-create on every
next/currentIndex change or keep a stable wrapper in a ref (e.g.,
startIntervalRef) that invokes the latest next via a mutable ref; update
useEffect to depend only on items.length and intervalRef, and call the stable
startInterval (from the useCallback or ref) so intervals are not
cleared/recreated on each slide transition.
src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx (1)

47-55: Consider adding accessibility attributes to theme selection buttons.

Screen reader users won't know which theme each button represents. Adding aria-label or aria-pressed would improve accessibility.

♿ Proposed accessibility improvement
 <button
   key={t.id}
   onClick={() => setTheme(t.id)}
+  aria-label={`Select ${t.name} theme`}
+  aria-pressed={isActive}
   className={`relative flex flex-col overflow-hidden rounded-xl border-2 text-left transition duration-200 focus:outline-none ${
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx`
around lines 47 - 55, The theme selection buttons in UserAppearanceSettings lack
accessibility attributes; update the button (the element rendering per theme
with key={t.id} and onClick={() => setTheme(t.id)}) to include an accessible
label (e.g., aria-label using the theme name like t.name or t.label) and expose
selection state via aria-pressed={isActive} (or aria-current when appropriate)
so screen readers announce which theme each button represents and which one is
active; keep existing visual styling and state logic (isActive, setTheme,
bgColor) and only add these ARIA attributes to the button element.
src/components/Common/Dropdown/index.tsx (1)

60-70: Inconsistent surface token usage for ghost variant.

Line 64 uses bg-surface-raised/90 for the ghost variant, which is a semantic token. However, line 67 for glass uses hardcoded bg-black/80. Consider using semantic tokens consistently, or document when hardcoded values are intentional.

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

In `@src/components/Common/Dropdown/index.tsx` around lines 60 - 70, The ghost
dropdown variant uses the semantic token bg-surface-raised/90 but the glass
variant uses a hardcoded bg-black/80; update the Menu.Items className
construction (the block referencing dropdownType, Menu.Items, dropdownType ===
'ghost' and dropdownType === 'glass') to use a consistent semantic token for the
glass variant (or a documented semantic token name) instead of bg-black/80,
e.g., replace the hardcoded color with the appropriate semantic token (or add a
comment documenting the intentional hardcoded value) and ensure className
merging still includes the provided className and props.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release.yml:
- Around line 129-151: In the "Configure target registries" step, normalize GHCR
image names to lowercase by using the lowercase form of GITHUB_REPOSITORY when
composing primary_image and the images list; update the echo statements that set
primary_image and append ghcr.io/${GITHUB_REPOSITORY} (used in the images<<EOF
block and the primary_image assignment) to use the lowercase expansion (e.g.,
${GITHUB_REPOSITORY,,}) so all ghcr.io image references are consistently
lowercased.

In `@src/components/Common/Tag/index.tsx`:
- Line 11: The tag component's root div in Tag (the inline-flex chip) uses fixed
palette classes ring-gray-600 and hover:bg-gray-700; replace those with
theme-aware semantic token classes (e.g., use your design system's
surface-border / surface-hover tokens such as ring-surface-border and
hover:bg-surface-hover or the equivalent CSS variable tokens) so the ring and
hover states follow the current surface/theme; update the class string on the
div in src/components/Common/Tag/index.tsx accordingly to use those semantic
token names instead of the gray-600/gray-700 classes.

In `@src/components/Discover/HeroSlider/index.tsx`:
- Around line 239-244: Replace the hardcoded "View Details" label with the i18n
message used elsewhere: add a message key (e.g. viewDetails) to the messages
object for components.Discover.HeroSlider (where defineMessages is used) and
then use intl.formatMessage(messages.viewDetails) inside the Link component that
renders the button (the Link with href={detailUrl}) so the label is localized.

In `@src/components/Layout/AmoledNavbar/index.tsx`:
- Around line 185-186: The conditional is comparing
user?.displayName?.toLowerCase() to user?.email without normalizing both sides,
causing inconsistent case-sensitive behavior; update the JSX conditional (the
expression that currently uses user?.displayName and user?.email) to compare
normalized values for both fields (e.g., compare (user?.displayName ||
'').trim().toLowerCase() !== (user?.email || '').trim().toLowerCase()), ensuring
you guard against undefined/null before calling toLowerCase().
- Around line 70-73: The logout function (logout) currently calls
axios.post('/api/v1/auth/logout') and revalidate() with no error handling; wrap
the call in a try/catch so network or server errors are caught, call
revalidate() only on successful response.data?.status === 'ok', and in the catch
block log the error (console.error or processLogger) and surface user feedback
(e.g., show an error toast or set an error state) so the failure doesn't
silently propagate; ensure the function still returns/handles promise resolves
correctly to avoid unhandled rejections.
- Around line 50-57: The effect in AmoledNavbar (useEffect that focuses
searchInputRef and calls clear() and setIsOpen()) is missing dependencies:
include clear and setIsOpen in the dependency array or memoize clear inside
useSearchInput; either add clear and setIsOpen to the useEffect deps
([searchExpanded, isDesktop, clear, setIsOpen]) or change the clear function in
useSearchInput to be stable using useCallback (exporting the stable clear) so
the existing dependency array stays valid.

In `@src/components/MediaSlider/index.tsx`:
- Around line 180-196: The "See all" label is hardcoded; update the MediaSlider
header to use intl.formatMessage for i18n: add a message id (e.g.
"mediaSlider.seeAll") to your messages bundle and replace the literal "See all"
inside the Link (the JSX that renders when linkUrl is truthy) with
intl.formatMessage({ id: 'mediaSlider.seeAll' }) — ensure the component imports
and calls useIntl() (or uses the existing intl) and keep the surrounding
elements (Link, ChevronRightIcon, title, linkUrl) unchanged.

In `@src/components/MovieDetails/index.tsx`:
- Line 1411: The four hardcoded section headings ("Crew", "Keywords", "Ratings",
"Details") in MovieDetails (the h3 elements with className "text-xs
font-semibold uppercase tracking-wider text-white/30") must be moved into the
component's messages and rendered via the intl API (e.g., intl.formatMessage or
<FormattedMessage>) instead of literal strings; add keys to the messages object
(e.g., crewLabel, keywordsLabel, ratingsLabel, detailsLabel), import/consume the
intl instance used elsewhere in this file, and replace the literal text in the
h3 elements with the localized message lookups so the AMOLED-only branch
translates correctly.
- Around line 434-436: The JSX return in MovieDetails is broken by a stray
closing ternary branch ") : (" without a matching opening condition; restore a
proper conditional around the two layout branches (e.g., replace the initial
fragment opening with a condition like someFlag ? ( ... ) : ( ... ) or delete
one branch so only one layout remains) by editing the MovieDetails component's
return block. Also in the AMOLED branch replace the hardcoded English headers
"Crew", "Keywords", "Ratings", and "Details" with localized calls using
intl.formatMessage (for example intl.formatMessage({ id: 'movie.crew',
defaultMessage: 'Crew' }) and similarly for keywords, ratings, details) so those
labels use the app i18n.

In `@src/components/Settings/OverrideRule/OverrideRuleTiles.tsx`:
- Around line 142-143: The list items rendered inside the rules.map in the
OverrideRuleTiles component lack a stable React key; update the JSX in the map
(the <li> produced for each rule in rules.map) to include a stable key prop
(preferably rule.id or another unique identifier on the rule object, falling
back to a composite stable string like `${rule.id || rule.name}` and only use
index as last resort) so React can correctly track rule tiles across
edits/deletes.

In `@src/components/Slider/index.tsx`:
- Around line 153-171: The two icon-only navigation buttons (the left/right
buttons that call slide(Direction.LEFT) and slide(Direction.RIGHT) and render
ChevronLeftIcon/ChevronRightIcon) lack accessible names; add accessible labels
by providing either an aria-label (e.g., aria-label="Previous slide" and
aria-label="Next slide") or include visually-hidden text inside the button so
screen readers can identify them, and ensure the attributes remain when disabled
(do not remove or conditionally hide the label based on scrollPos.isStart or
scrollPos.isEnd).

In `@src/components/TitleCard/index.tsx`:
- Around line 500-513: Duplicate data-testid "title-card-title" is used for both
AMOLED and standard title blocks in the TitleCard component; update the test ids
to be unique by changing the AMOLED block's data-testid (identified by the
container className "title-card-amoled-title-block" and the h1 using className
"mb-1 whitespace-normal text-sm font-bold leading-tight text-white") to
something like "title-card-title-amoled" and change the standard title block's
h1 to "title-card-title-standard" (or similar unique names) so tests can target
each element unambiguously.
- Around line 377-383: The icon-only AMOLED action buttons (e.g., the <button>
wrapping <StarIcon> and the <Button> with buttonType={'ghost'}) lack accessible
names; update those elements and the similar remove-watchlist and blocklist icon
buttons (lines referenced around the StarIcon and Button usage) to include
type="button" and an appropriate aria-label (e.g., aria-label="Add to watchlist"
/ "Remove from watchlist" / "Block user") so screen readers can announce their
purpose, keeping their existing onClick handlers like onClickWatchlistBtn
intact; ensure you add aria-label to both native <button> elements and the
custom <Button> component props if it forwards attributes.

In `@src/components/TvDetails/index.tsx`:
- Line 1670: The headings "Crew", "Keywords", "Ratings", and "Details" are
hardcoded in the TvDetails component and must be localized; replace the literal
strings in the JSX (the h3 elements that render "Crew", "Keywords", "Ratings",
"Details") with calls to intl.formatMessage using new message entries (e.g.,
tvDetails.crew, tvDetails.keywords, tvDetails.ratings, tvDetails.details) and
add those IDs to your messages file; ensure you import/use the existing intl
instance in this component and render intl.formatMessage({ id: 'tvDetails.crew'
}) (and the corresponding IDs for the other three headings) where the current
text nodes are.
- Around line 465-467: The returned JSX currently contains a broken branch
switch (a stray ") : (" without a matching "?") between the standard and AMOLED
layout branches inside the fragment that begins with the div having className
"tv-details-backdrop-section media-page"; fix this by wrapping the two layout
branches in a proper ternary expression (e.g., isAmoled ? (/* AMOLED branch */)
: (/* standard branch */)) so the fragment parses correctly and remove the stray
separator comment that Biome flagged; additionally replace the hardcoded English
labels "Crew", "Ratings", and "Details" in the AMOLED branch with react-intl
usage (useFormattedMessage or <FormattedMessage id="tv.crew"/>, id="tv.ratings",
id="tv.details" or the project's equivalent intl.formatMessage calls) so
localization is preserved.

In `@src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx`:
- Around line 43-56: The code destructures t.swatches inside THEMES.map (const
[bgColor, surfaceColor, accentColor] = t.swatches) but doesn't guard against
shorter arrays, causing undefined colors to be applied; fix by adding a
defensive check/fallback: when rendering each theme in the THEMES.map callback
(where isActive, setTheme, theme are used), ensure t.swatches exists and has at
least three entries and supply sensible default values (or fallback the single
bgColor used in style to a default like 'transparent') so style={{ background:
... }} never receives undefined; update the destructuring or the style
expression to use these fallback values (referencing t.swatches, bgColor,
surfaceColor, accentColor in that component).

In `@src/context/ThemeContext.tsx`:
- Around line 42-49: Read localStorage synchronously before React mounts and use
that value as the initial state so the saved theme is applied before first
paint: implement a module-level resolver that reads STORAGE_KEY and validates
against THEMES (falling back to DEFAULT_THEME), use that resolved value as the
initial argument to useState (instead of DEFAULT_THEME) in ThemeContext (so
setThemeState starts with the persisted theme), and ensure you still set
document.documentElement.setAttribute('data-theme', resolved) immediately after
resolving so the <html> attribute is updated before hydration; keep references
to STORAGE_KEY, THEMES, DEFAULT_THEME, setThemeState, and
document.documentElement.setAttribute when making the change.
- Around line 13-25: THEMES currently embeds hardcoded English display strings
(name/description) which breaks localization; update the ThemeDefinition entries
to only include stable identifiers (keep id and swatches) or add explicit
translation keys (e.g. nameKey/descriptionKey) instead of English text, then
change any UI that reads THEMES (the appearance picker / theme renderer) to call
the i18n translation function with those keys (for example
i18n.t(`themes.${id}.name`) and i18n.t(`themes.${id}.description`)) so
translations are provided at render time; ensure ThemeDefinition type is updated
accordingly and replace uses of name/description with the translated strings.

In `@src/styles/globals.css`:
- Around line 653-659: The gradient uses Tailwind's theme('colors.gray.900')
which isn't valid in plain CSS; update the rule targeting
[data-theme='amoled-strix'] .collection-backdrop-overlay and
.issue-backdrop-overlay to use a concrete color value or CSS variable (e.g.,
replace theme('colors.gray.900') with the hex for gray-900 or with
var(--color-gray-900) / var(--tw-gradient-to) if you provide that variable) so
the linear-gradient resolves correctly in runtime.

---

Outside diff comments:
In `@src/components/Login/JellyfinLogin.tsx`:
- Around line 124-130: The background utility on the input Fields is being
overridden by the input[type='text'] / input[type='password'] selector, so
restore the important flag by changing the className on the Field with
id="username" (and the corresponding Field with id="password") in the
JellyfinLogin component to use "!bg-input" instead of "bg-input" (keep the other
classes like "placeholder:text-gray-400"); this mirrors the existing fix used in
RequestList.tsx and ensures the intended background class wins specificity.

In `@src/pages/_app.tsx`:
- Around line 203-223: The app flashes the wrong theme because ThemeProvider
only sets the data-theme inside a client-side useEffect; to fix, add an inline
script in the custom document that runs before paint and sets
document.documentElement.setAttribute('data-theme', <value>) by reading the same
localStorage key your ThemeProvider uses (or alternatively pass the persisted
theme from SSR into currentSettings and render it on the server); target the
ThemeProvider/data-theme behavior and ensure the inline script reads the
identical storage key and sets the attribute synchronously so the correct theme
is present on first paint.

---

Nitpick comments:
In `@src/components/Blocklist/index.tsx`:
- Around line 116-117: The filter/search icon holders in
src/components/Blocklist (the span wrapping <FunnelIcon> and the similar span
for the search icon) still use hardcoded classes "border-gray-500 bg-gray-800";
update those className values to use the semantic tokens used in RequestList
(e.g., replace "border-gray-500 bg-gray-800" with "border-border-default
bg-input") so the components follow theme tokens and remain consistent across
dark/light themes; ensure you update both occurrences (the FunnelIcon holder and
the search icon holder) in the Blocklist component.

In `@src/components/CollectionDetails/index.tsx`:
- Around line 203-214: The overlay currently hard-codes Tailwind utility
gradient colors in the JSX; update the JSX in CollectionDetails (the element
with className "collection-backdrop-overlay") to remove the inline utilities
"bg-gradient-to-b from-gray-900/50 to-gray-900" so the element only references
the CSS class, then move the full gradient definition into the stylesheet (or
replace the utilities with theme-aware tokens) for
".collection-backdrop-overlay" so the backdrop tint is controlled by theme-aware
CSS rather than hard-coded palette utilities.

In `@src/components/Common/Dropdown/index.tsx`:
- Around line 60-70: The ghost dropdown variant uses the semantic token
bg-surface-raised/90 but the glass variant uses a hardcoded bg-black/80; update
the Menu.Items className construction (the block referencing dropdownType,
Menu.Items, dropdownType === 'ghost' and dropdownType === 'glass') to use a
consistent semantic token for the glass variant (or a documented semantic token
name) instead of bg-black/80, e.g., replace the hardcoded color with the
appropriate semantic token (or add a comment documenting the intentional
hardcoded value) and ensure className merging still includes the provided
className and props.

In `@src/components/Common/ImageFader/index.tsx`:
- Around line 26-27: Replace the hard-coded string comparison in ImageFader
(const { theme } = useTheme(); const isAmoled = theme === 'amoled-strix') with a
reference to the centralized theme identifier constant (e.g.,
ThemeId.AMOLED_STRIX or THEMES.AMOLED_STRIX): import the appropriate constant
from the shared theme definitions and use theme === <THEME_CONST> to set
isAmoled so the component stays in sync with central theme enums/IDs.

In `@src/components/Common/Modal/index.tsx`:
- Line 119: The modal component still uses hardcoded gray tokens in the
container and backdrop (see the className on the modal container that includes
"ring-gray-700" and the backdrop gradient classes referencing "from-gray-800/75"
and "to-gray-800"); replace those fixed color classes with the semantic token
equivalents used by the theme (e.g., use the project's semantic ring/backdrop
tokens instead of ring-gray-700, from-gray-800/75, to-gray-800) so the modal
respects AMOLED-Strix theming—update the className string on the modal container
and the backdrop gradient construction (references: dialogClass and the modal
backdrop element) to use the appropriate semantic token classes.

In `@src/components/Common/SlideOver/index.tsx`:
- Line 74: The hard-coded color token "ring-gray-700" in the SlideOver
component's container div should be replaced with your design system's semantic
ring token to ensure theme consistency; update the className on the div in
src/components/Common/SlideOver/index.tsx (the line containing the JSX: div
className={`flex h-full flex-col rounded-lg shadow-xl ring-1 backdrop-blur
bg-surface ring-gray-700`}) to use the semantic token (for example replace
ring-gray-700 with your semantic token such as ring-border or ring-ring) so the
ring color follows theming across themes.

In `@src/components/Common/Tooltip/index.tsx`:
- Line 28: The tooltip CSS currently uses a hardcoded class "border-gray-600" in
the Tooltip component (src/components/Common/Tooltip/index.tsx) which bypasses
theme tokens; update the class string in the Tooltip (the line that contains
`z-50 text-sm absolute font-normal px-2 py-1 rounded border shadow text-gray-100
bg-surface-raised border-gray-600`) to use the semantic border token used by the
design system (replace `border-gray-600` with the appropriate theme token, e.g.
`border-surface-border` or `border-border`) so the tooltip border color follows
theme tokens across themes.

In `@src/components/Discover/DiscoverMovies/index.tsx`:
- Around line 70-77: The backdrop selection useEffect in DiscoverMovies
duplicates the same logic used in DiscoverTv; extract it into a shared hook like
useRandomBackdrop<T extends { backdropPath?: string }>(items: T[] | undefined):
string | null and replace the effect: move filtering for items with
backdropPath, random pick, and pickedRef handling into the hook and return the
chosen backdrop; then in this file remove the useEffect and pickedRef usage and
call the hook to get the backdrop value and pass it to setBackdropPath (or
directly use the returned value), keeping references to useEffect, pickedRef,
setBackdropPath, and MovieResult to locate the original logic and ensure both
DiscoverMovies and DiscoverTv consume the new hook for consistent behavior.

In `@src/components/Discover/DiscoverTv/index.tsx`:
- Around line 70-72: The filter and pick logic redundantly cast each item to
TvResult even though titles is already TvResult[] from useDiscover; remove the
unnecessary casts by changing the filter to const withBackdrop = titles.filter(t
=> t.backdropPath); and keep pick as const pick =
withBackdrop[Math.floor(Math.random() * withBackdrop.length)] as TvResult (or
drop the cast there too since withBackdrop is TvResult[]), ensuring you
reference the withBackdrop, titles and TvResult identifiers when making the
change.

In `@src/components/Discover/HeroSlider/index.tsx`:
- Around line 75-80: The effect is recreating the interval whenever
startInterval changes (because startInterval captures next/currentIndex),
causing interval churn; stabilize the timer by removing the changing callback
from the effect deps: either memoize startInterval with useCallback so it
doesn't re-create on every next/currentIndex change or keep a stable wrapper in
a ref (e.g., startIntervalRef) that invokes the latest next via a mutable ref;
update useEffect to depend only on items.length and intervalRef, and call the
stable startInterval (from the useCallback or ref) so intervals are not
cleared/recreated on each slide transition.

In `@src/components/Discover/Trending.tsx`:
- Around line 55-57: In the Trending component, replace unnecessary template
literal className values with plain strings for the icon-wrapper spans (the span
containing CircleStackIcon and the other span around the second icon at lines
~77-79) — update className={`inline-flex cursor-default items-center
rounded-l-md border border-r-0 px-3 text-sm text-gray-100 border-border-default
bg-input`} and the similar occurrence to simply use "inline-flex cursor-default
items-center rounded-l-md border border-r-0 px-3 text-sm text-gray-100
border-border-default bg-input" (i.e., remove backticks and curly braces) so
there is no template literal when no interpolation is present.

In `@src/components/IssueModal/CreateIssueModal/index.tsx`:
- Line 247: In the CreateIssueModal component, replace the unnecessary template
literal used for the div's className (currently written as
className={`-space-y-px overflow-hidden rounded-md bg-surface/50`}) with a plain
string literal " -space-y-px overflow-hidden rounded-md bg-surface/50" (i.e.,
className="-space-y-px overflow-hidden rounded-md bg-surface/50") since there is
no interpolation; locate the JSX div with that className and update it
accordingly to reduce noise.

In `@src/components/Layout/PullToRefresh/index.tsx`:
- Line 120: The JSX in the PullToRefresh component (the element whose className
contains "relative -top-28 h-9 w-9 rounded-full border-4 shadow-md shadow-black
ring-1 border-gray-800 bg-surface-raised ring-gray-700") uses hardcoded color
classes; replace border-gray-800 and ring-gray-700 with your theme token classes
(e.g., the project’s tokenized border and ring utilities such as
border-surface-stroke and ring-surface-outline or the equivalent token names in
your design system) so the component uses tokenized colors consistently with
bg-surface-raised; update the className string in the PullToRefresh render (the
template literal containing those classes) to use the token names.

In `@src/components/PersonDetails/index.tsx`:
- Around line 245-266: Extract the repeated backdrop URL pipeline into a
memoized value (e.g., const backdropUrls = useMemo(() => ..., [sortedCast,
sortedCrew])) and replace the two inline arrays passed to ImageFader with that
variable; specifically compute backdropUrls by concatenating [...(sortedCast ??
[]), ...(sortedCrew ?? [])], filtering for media.backdropPath, mapping to the
TMDB URL string, and slicing(0, 6), then pass backdropUrls to both ImageFader
instances to remove duplication while depending on sortedCast and sortedCrew.

In `@src/components/RequestCard/index.tsx`:
- Line 53: RequestCardPlaceholder and RequestCardError use hardcoded tailwind
classes bg-gray-700 and bg-gray-800; change them to use the semantic token class
used by RequestCard (e.g., bg-surface or the project's semantic token like
bg-surface-500) so theming is consistent. Locate the RequestCardPlaceholder
component (the div with className "relative w-72 animate-pulse rounded-xl
bg-gray-700 p-4 sm:w-96") and RequestCardError (the element using "bg-gray-800")
and replace those bg-* classes with the semantic token class used by RequestCard
(and update any pulse/spacing classes unchanged), ensuring both placeholders and
error states follow the same semantic background token.

In `@src/components/RequestList/index.tsx`:
- Line 276: The hover style in the className on the RequestList component uses a
hardcoded color token ("hover:!bg-gray-400"); update that to use the project's
semantic token instead (replace "hover:!bg-gray-400" with the appropriate
semantic class/variable used elsewhere, e.g., "hover:!bg-surface-hover" or a CSS
variable like "hover:!bg-[var(--bg-hover)]") so the element follows theme
tokens; edit the className string in src/components/RequestList/index.tsx where
the class includes "z-40 mr-2 rounded-l-none border border-border-default
!bg-input !px-3 !text-gray-500 hover:!bg-gray-400 hover:!text-white" and swap
the hardcoded hover token for the project's semantic hover token.

In `@src/components/Toast/index.tsx`:
- Line 30: In the Toast component replace the hard-coded palette class
"ring-gray-500" on the outer div (the div with className `pointer-events-auto
w-full max-w-sm rounded-lg shadow-lg ring-1 bg-surface ...`) with the semantic
ring token used across the design system (for example "ring-outline" or the
project's equivalent), i.e., update the class string inside the Toast component
to use the semantic token instead of "ring-gray-500" so theme tokens and AMOLED
remapping apply consistently.

In `@src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx`:
- Around line 47-55: The theme selection buttons in UserAppearanceSettings lack
accessibility attributes; update the button (the element rendering per theme
with key={t.id} and onClick={() => setTheme(t.id)}) to include an accessible
label (e.g., aria-label using the theme name like t.name or t.label) and expose
selection state via aria-pressed={isActive} (or aria-current when appropriate)
so screen readers announce which theme each button represents and which one is
active; keep existing visual styling and state logic (isActive, setTheme,
bgColor) and only add these ARIA attributes to the button element.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89ba5e68-af40-4239-8af4-222cdc7d835a

📥 Commits

Reviewing files that changed from the base of the PR and between d25d0ca and e1b55e6.

📒 Files selected for processing (71)
  • .github/workflows/release.yml
  • src/components/Blocklist/index.tsx
  • src/components/CollectionDetails/index.tsx
  • src/components/Common/ButtonWithDropdown/index.tsx
  • src/components/Common/Dropdown/index.tsx
  • src/components/Common/ImageFader/index.tsx
  • src/components/Common/Modal/index.tsx
  • src/components/Common/PlayButton/index.tsx
  • src/components/Common/SettingsTabs/index.tsx
  • src/components/Common/SlideOver/index.tsx
  • src/components/Common/Table/index.tsx
  • src/components/Common/Tag/index.tsx
  • src/components/Common/Tooltip/index.tsx
  • src/components/Discover/DiscoverMovies/index.tsx
  • src/components/Discover/DiscoverSliderEdit/index.tsx
  • src/components/Discover/DiscoverTv/index.tsx
  • src/components/Discover/HeroSlider/index.tsx
  • src/components/Discover/Trending.tsx
  • src/components/Discover/index.tsx
  • src/components/IssueDetails/IssueComment/index.tsx
  • src/components/IssueDetails/index.tsx
  • src/components/IssueList/IssueItem/index.tsx
  • src/components/IssueList/index.tsx
  • src/components/IssueModal/CreateIssueModal/index.tsx
  • src/components/Layout/AmoledNavbar/index.tsx
  • src/components/Layout/PullToRefresh/index.tsx
  • src/components/Layout/UserDropdown/index.tsx
  • src/components/Layout/index.tsx
  • src/components/Login/JellyfinLogin.tsx
  • src/components/Login/LocalLogin.tsx
  • src/components/Login/index.tsx
  • src/components/MediaSlider/index.tsx
  • src/components/MovieDetails/index.tsx
  • src/components/PersonDetails/index.tsx
  • src/components/RegionSelector/index.tsx
  • src/components/RequestBlock/index.tsx
  • src/components/RequestButton/index.tsx
  • src/components/RequestCard/index.tsx
  • src/components/RequestList/RequestItem/index.tsx
  • src/components/RequestList/index.tsx
  • src/components/RequestModal/AdvancedRequester/index.tsx
  • src/components/ResetPassword/RequestResetLink.tsx
  • src/components/ResetPassword/index.tsx
  • src/components/Settings/OverrideRule/OverrideRuleTiles.tsx
  • src/components/Settings/SettingsAbout/Releases/index.tsx
  • src/components/Settings/SettingsJellyfin.tsx
  • src/components/Settings/SettingsLogs/index.tsx
  • src/components/Settings/SettingsMetadata.tsx
  • src/components/Settings/SettingsPlex.tsx
  • src/components/Settings/SettingsServices.tsx
  • src/components/Setup/JellyfinSetup.tsx
  • src/components/Setup/index.tsx
  • src/components/Slider/index.tsx
  • src/components/TitleCard/ErrorCard.tsx
  • src/components/TitleCard/index.tsx
  • src/components/Toast/index.tsx
  • src/components/TvDetails/Season/index.tsx
  • src/components/TvDetails/index.tsx
  • src/components/UserList/index.tsx
  • src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem.tsx
  • src/components/UserProfile/UserSettings/index.tsx
  • src/components/UserProfile/index.tsx
  • src/context/ThemeContext.tsx
  • src/hooks/useTheme.ts
  • src/pages/_app.tsx
  • src/pages/profile/settings/appearance.tsx
  • src/pages/users/[userId]/settings/appearance.tsx
  • src/styles/globals.css
  • tailwind.config.js

Comment thread .github/workflows/release.yml Outdated
Comment thread src/components/Common/Tag/index.tsx Outdated
Comment thread src/components/Discover/HeroSlider/index.tsx
Comment thread src/components/Layout/AmoledNavbar/index.tsx Outdated
Comment thread src/components/Layout/AmoledNavbar/index.tsx Outdated
Comment thread src/components/TvDetails/index.tsx Outdated
Comment thread src/context/ThemeContext.tsx
Comment thread src/context/ThemeContext.tsx Outdated
Comment thread src/styles/globals.css
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
src/components/Settings/OverrideRule/OverrideRuleTiles.tsx (1)

155-163: ⚠️ Potential issue | 🟡 Minor

Add key props to inner map iterations.

Multiple nested .map() calls render elements without keys (e.g., lines 155-163 for users, 174-178 for genres, 191-201 for languages, 212-221 for keywords, 259-271 for tags). While less critical than the outer <li>, these should have stable keys for correct reconciliation.

Example fix for users:

                    {rule.users.split(',').map((userId) => {
                      return (
-                        <span>
+                        <span key={userId}>
                          {
                            users?.find((user) => user.id === Number(userId))
                              ?.displayName
                          }
                        </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Settings/OverrideRule/OverrideRuleTiles.tsx` around lines 155
- 163, The inner .map() iterations inside the OverrideRuleTiles component render
spans without key props (e.g., the users map over rule.users, genres map,
languages map, keywords map, and tags map), causing unstable reconciliation;
update each mapped element (the span or fragment returned in the users.map,
genres.map, languages.map, keywords.map, and tags.map callbacks) to include a
stable key prop derived from the item (use userId or the mapped id/string such
as Number(userId), genre id/name, language code/id, keyword id/text, or tag id)
so React can track them reliably (ensure keys are unique within each list and
use the parsed Number(userId) or the original identifier as appropriate).
♻️ Duplicate comments (10)
src/components/Common/Tag/index.tsx (1)

11-11: ⚠️ Potential issue | 🟡 Minor

Use semantic ring/hover tokens so Tag stays theme-consistent.

Line 11 still mixes bg-surface with fixed palette classes (ring-gray-600, hover:bg-gray-700), so ring/hover won’t adapt to AMOLED-Strix.

Suggested update
-    <div className={`inline-flex cursor-pointer items-center rounded-full px-2 py-1 text-sm text-gray-200 ring-1 transition bg-surface ring-gray-600 hover:bg-gray-700`}>
+    <div className="inline-flex cursor-pointer items-center rounded-full bg-surface px-2 py-1 text-sm text-gray-200 ring-1 ring-ring-app transition hover:bg-surface-raised">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Common/Tag/index.tsx` at line 11, The Tag component's root div
(in src/components/Common/Tag/index.tsx — the inline-flex cursor-pointer
items-center rounded-full ... element) uses fixed palette classes (ring-gray-600
and hover:bg-gray-700) which break theming; replace those fixed classes with the
project's semantic tokens (e.g., use the theme's ring and hover background
tokens such as ring-<semantic-token> and hover:bg-<semantic-token> — e.g.,
ring-border/ ring-muted and hover:bg-surface-hover or the equivalent tokens your
design system defines) so the ring and hover states adapt to AMOLED-Strix and
other themes while keeping the rest of the class list intact.
src/components/MediaSlider/index.tsx (1)

192-192: ⚠️ Potential issue | 🟡 Minor

Localize hardcoded “See all” label.

Line 192 still uses a hardcoded user-facing string, so this won’t be translated in non-English locales.

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

In `@src/components/MediaSlider/index.tsx` at line 192, Replace the hardcoded "See
all" string in the MediaSlider component with a localized message: import and
use your app's i18n helper (e.g., useTranslation from 'react-i18next' or the
project's t() utility) inside the MediaSlider component and replace the literal
"See all" JSX text with t('mediaSlider.seeAll') (or the project's chosen key);
also add the 'mediaSlider.seeAll' entry to the locale resource files for
supported languages so translations are available.
src/components/Discover/HeroSlider/index.tsx (1)

239-244: ⚠️ Potential issue | 🟡 Minor

Hardcoded "View Details" text should be internationalized.

The "View Details" button text is hardcoded in English while other UI text uses intl.formatMessage. This breaks i18n consistency.

🌐 Proposed fix

Add to messages definition:

import defineMessages from '@app/utils/defineMessages';

const messages = defineMessages('components.Discover.HeroSlider', {
  viewDetails: 'View Details',
});

Then use it:

-View Details
+{intl.formatMessage(messages.viewDetails)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Discover/HeroSlider/index.tsx` around lines 239 - 244, The
"View Details" text in the Link inside the HeroSlider is hardcoded; add a
message key (e.g., viewDetails) to the component's messages definition (use
defineMessages with namespace 'components.Discover.HeroSlider') and replace the
literal "View Details" with intl.formatMessage(messages.viewDetails) in the Link
(the element using href={detailUrl}); this aligns with existing i18n usage in
the component.
src/components/MovieDetails/index.tsx (2)

1411-1411: ⚠️ Potential issue | 🟡 Minor

Localize the AMOLED section headings.

Lines 1411, 1435, 1458, and 1496 hardcode "Crew", "Keywords", "Ratings", and "Details" in English. The standard layout uses intl.formatMessage() for these labels; the AMOLED branch should do the same.

🌐 Proposed fix

The existing messages object already has cast but not these labels. Add them:

const messages = defineMessages('components.MovieDetails', {
  // ... existing messages
  crew: 'Crew',
  keywords: 'Keywords', 
  ratings: 'Ratings',
  details: 'Details',
});

Then replace hardcoded strings:

-<h3 className="...">Crew</h3>
+<h3 className="...">{intl.formatMessage(messages.crew)}</h3>

-<h3 className="...">Keywords</h3>
+<h3 className="...">{intl.formatMessage(messages.keywords)}</h3>

-<h2 className="...">Ratings</h2>
+<h2 className="...">{intl.formatMessage(messages.ratings)}</h2>

-<h2 className="...">Details</h2>
+<h2 className="...">{intl.formatMessage(messages.details)}</h2>

Also applies to: 1435-1435, 1458-1458, 1496-1496

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

In `@src/components/MovieDetails/index.tsx` at line 1411, Add localized labels for
the AMOLED headings by extending the existing messages object (identifier:
messages) with keys crew, keywords, ratings, and details and then replace the
hardcoded strings "Crew", "Keywords", "Ratings", and "Details" in the JSX with
intl.formatMessage(messages.crew), intl.formatMessage(messages.keywords),
intl.formatMessage(messages.ratings), and intl.formatMessage(messages.details)
respectively; update usages near the h3 headings (the elements rendering those
uppercase labels) so they pull from intl.formatMessage instead of literal
strings.

1136-1139: ⚠️ Potential issue | 🔴 Critical

Critical: Broken ternary operator - missing opening condition.

Line 1137 has ) : ( which implies a ternary expression, but the return statement at line 434 opens with <> (a fragment), not a conditional. This creates invalid JSX syntax that will fail to compile.

The structure appears to be:

return (
  <>
    <div className="movie-details-backdrop-section ...">
      {/* ~700 lines of standard layout */}
    </div>
    ) : (           // ← ERROR: No matching ternary opening
    <div className="relative bg-black">
      {/* AMOLED layout */}
    </div>
  </>
);

You need to add the opening condition before the standard layout:

🐛 Proposed fix structure
  return (
-   <>
-     <div className="movie-details-backdrop-section ...">
+   isAmoledTheme ? (
+     <div className="relative bg-black">
+       {/* AMOLED layout */}
+     </div>
+   ) : (
+     <div className="movie-details-backdrop-section ...">
        {/* Standard layout */}
      </div>
-     ) : (
-     <div className="relative bg-black">
-       {/* AMOLED layout */}
-     </div>
-   </>
+   )
  );

Or wrap with the theme check:

const { theme } = useTheme();
const isAmoled = theme === 'amoled-strix';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MovieDetails/index.tsx` around lines 1136 - 1139, The JSX
contains a stray ternary closing token ") : (" in the MovieDetails component
causing a syntax error; fix it by introducing a proper conditional around the
two layout branches (standard vs AMOLED) — e.g., get theme from useTheme()
(const { theme } = useTheme()) and compute isAmoled (theme === 'amoled-strix'),
then replace the fragment-wrapped pair with a ternary expression using isAmoled
? <div className="relative bg-black">…</div> : <div
className="movie-details-backdrop-section …">…</div> (or invert the order to
match intended UX) so the opening condition matches the existing ") : (" and
removes the stray token.
src/styles/globals.css (1)

657-663: ⚠️ Potential issue | 🟠 Major

Invalid use of theme() function in vanilla CSS.

The theme('colors.gray.900') function on line 662 is a Tailwind directive that only works within @apply or Tailwind's processing context. In a standard CSS rule, this will output literally as theme('colors.gray.900') and fail to resolve.

🐛 Proposed fix using hardcoded RGB value
 [data-theme='amoled-strix'] .collection-backdrop-overlay,
 [data-theme='amoled-strix'] .issue-backdrop-overlay {
   --tw-gradient-from: rgba(0, 0, 0, 0.6);
   --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
-  background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), theme('colors.gray.900'));
+  background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgb(17 24 39));
 }

Or use the CSS variable for theme-awareness:

-  background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), theme('colors.gray.900'));
+  background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgb(var(--gray-900)));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/styles/globals.css` around lines 657 - 663, The CSS rule for the
selectors .collection-backdrop-overlay and .issue-backdrop-overlay uses
Tailwind's theme('colors.gray.900') which won't be resolved in plain CSS;
replace that call with a concrete color value or a CSS variable (for example the
equivalent rgba/hex for gray.900 or var(--color-gray-900)) and ensure
--tw-gradient-to is set if you want theme-aware behavior so the background-image
uses a valid color instead of theme('colors.gray.900').
src/components/TvDetails/index.tsx (2)

360-362: ⚠️ Potential issue | 🔴 Critical

Critical syntax error: broken ternary expression.

The code at line 1360 has ) : ( without a corresponding ? conditional operator. The fragment opens at line 465 with <>, the standard layout ends at line 1359, then line 1360 attempts to switch branches with ) : (, but there's no ternary condition. This will cause a parse error and the component won't compile.

Additionally, line 1361 has a bare comment that Biome flags - JSX comments must be wrapped in braces: {/* AMOLED full-bleed layout */}.

You need to introduce a conditional (e.g., isAmoled ? (...) : (...)) to properly switch between the standard and AMOLED layouts.

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

In `@src/components/TvDetails/index.tsx` around lines 360 - 362, The JSX currently
has a broken ternary in the TvDetails component: replace the stray `) : (`
branch switch with a proper conditional using a boolean (e.g., isAmoled) so the
component renders either the standard layout or the AMOLED layout (turn the
fragment split into isAmoled ? (standard JSX) : (amoled JSX)); ensure the
boolean isAmoled is defined/derived in TvDetails (or passed as a prop) and use
it in the ternary, and convert the bare comment "AMOLED full-bleed layout" into
a JSX comment wrapped in braces `{/* AMOLED full-bleed layout */}` so the file
parses correctly.

1670-1670: ⚠️ Potential issue | 🟡 Minor

Localize the AMOLED section headings.

The AMOLED branch hardcodes English strings for section headings: "Crew" (line 1670), "Keywords" (line 1708), "Ratings" (line 1730), and "Details" (line 1775). These bypass react-intl and will not be translated.

Add message definitions and use intl.formatMessage():

-<h3 className="text-xs font-semibold uppercase tracking-wider text-white/30">Crew</h3>
+<h3 className="text-xs font-semibold uppercase tracking-wider text-white/30">{intl.formatMessage(messages.crew)}</h3>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TvDetails/index.tsx` at line 1670, The hardcoded section
headings ("Crew", "Keywords", "Ratings", "Details") in the TvDetails component
should be replaced with localized messages; add message identifiers (e.g.
tvDetails.crew/tvDetails.keywords/tvDetails.ratings/tvDetails.details) to the
messages file and use intl.formatMessage({ id: 'tvDetails.crew', defaultMessage:
'Crew' }) (or equivalent ids/defaults) in the TvDetails component where the h3
headings are rendered (ensure the component has access to the intl object via
useIntl or injectIntl). Update the message definitions for those four keys and
replace the literal strings with intl.formatMessage calls so the headings are
translatable.
src/components/TitleCard/index.tsx (2)

510-543: ⚠️ Potential issue | 🟡 Minor

Differentiate data-testid values between layout variants.

Both the AMOLED title (line 510) and standard title (line 543) use data-testid="title-card-title". This can break test queries that expect unique IDs.

🧪 Suggested fix
-data-testid="title-card-title"  // line 510
+data-testid="title-card-title-amoled"

-data-testid="title-card-title"  // line 543
+data-testid="title-card-title-standard"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TitleCard/index.tsx` around lines 510 - 543, Two h1 elements
in the TitleCard component share the same data-testid ("title-card-title")
causing conflicting test queries; update them to distinct values (for example
change the AMOLED/first h1 to "title-card-title-amoled" and the standard/second
h1 inside the title-card-standard-title-block to "title-card-title-standard") by
locating the h1 with data-testid in the AMOLED title block and the h1 inside the
element with className "title-card-standard-title-block", change their
data-testid strings accordingly, and then update any tests that reference the
old test id to use the new specific ids.

377-416: ⚠️ Potential issue | 🟡 Minor

Add accessible labels to icon-only buttons.

The AMOLED icon buttons at lines 377-382, 389-394, and 406-411 lack accessible names. Screen reader users won't understand their purpose.

♿ Suggested fix
 <button
+  type="button"
+  aria-label={intl.formatMessage(messages.addToWatchList)}
   className="title-card-amoled-icon-btn z-40 flex h-7 w-7 items-center justify-center rounded-full bg-black/50 backdrop-blur-md ring-1 ring-white/[0.12] transition hover:bg-black/70"
   onClick={onClickWatchlistBtn}
 >

Apply the same pattern (type="button" + aria-label) to the remove-watchlist button (line 389) and blocklist button (line 406).

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

In `@src/components/TitleCard/index.tsx` around lines 377 - 416, The AMOLED
icon-only <button> elements for removing from watchlist and for blocklist/hide
lack accessible names; update the two plain <button> elements that call
onClickDeleteWatchlistBtn and setShowBlocklistModal (the ones with className
"title-card-amoled-icon-btn") to include type="button" and a descriptive
aria-label (e.g., aria-label="Remove from watchlist" for
onClickDeleteWatchlistBtn and aria-label="Hide title" or "Block title" for
setShowBlocklistModal) so screen readers can announce their purpose; keep the
existing visual content and handlers unchanged.
🧹 Nitpick comments (6)
src/components/Common/Tooltip/index.tsx (1)

28-28: Use semantic border utility to keep tooltip fully theme-aware.

Line 28 still hardcodes border-gray-600, which bypasses theme token remapping. Use a semantic border class so the tooltip border tracks AMOLED/default theme tokens.

Proposed tweak
-    `z-50 text-sm absolute font-normal px-2 py-1 rounded border shadow text-gray-100 bg-surface-raised border-gray-600`,
+    `z-50 text-sm absolute font-normal px-2 py-1 rounded border shadow text-gray-100 bg-surface-raised border-border-default`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Common/Tooltip/index.tsx` at line 28, The tooltip class string
currently hardcodes "border-gray-600"; update the Tooltip component's className
string (the template literal that includes "z-50 text-sm absolute font-normal
px-2 py-1 rounded border shadow text-gray-100 bg-surface-raised
border-gray-600") to use the semantic border utility (for example replace
"border-gray-600" with a theme token class such as "border-surface-border") so
the tooltip border follows theme token remapping.
src/components/Common/SlideOver/index.tsx (1)

74-74: Consider migrating ring-gray-700 to ring-ring-app/10 for consistency.

The bg-surface migration is correct, but ring-gray-700 remains hardcoded while other components in this PR (e.g., OverrideRuleTiles, SettingsMetadata, Releases, UserLinkedAccountsSettings) use the semantic ring-ring-app/10 token.

Suggested fix
-                <div className={`flex h-full flex-col rounded-lg shadow-xl ring-1 backdrop-blur bg-surface ring-gray-700`}>
+                <div className="flex h-full flex-col rounded-lg shadow-xl ring-1 backdrop-blur bg-surface ring-ring-app/10">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Common/SlideOver/index.tsx` at line 74, The div in the
SlideOver component currently hardcodes the tailwind class ring-gray-700; update
the class list in the JSX element (the div with className starting "flex h-full
flex-col rounded-lg shadow-xl ring-1 backdrop-blur bg-surface") to replace
ring-gray-700 with the semantic token ring-ring-app/10 so it matches other
components (e.g., OverrideRuleTiles, SettingsMetadata) and keeps styling
consistent.
src/components/Common/Modal/index.tsx (1)

119-119: Consider using the semantic ring token for consistency.

The dialog uses bg-surface for the background but retains ring-gray-700 instead of using the semantic ring-ring-app token. Other components in this PR (e.g., DeviceItem, RequestItem) use ring-ring-app/10 for consistency with the theme system.

♻️ Suggested fix
-          className={`hide-scrollbar relative inline-block w-full overflow-auto px-4 pb-4 pt-4 text-left align-bottom shadow-xl ring-1 transition-all sm:my-8 sm:max-w-3xl sm:rounded-lg sm:align-middle bg-surface ring-gray-700 ${dialogClass}`}
+          className={`hide-scrollbar relative inline-block w-full overflow-auto px-4 pb-4 pt-4 text-left align-bottom shadow-xl ring-1 transition-all sm:my-8 sm:max-w-3xl sm:rounded-lg sm:align-middle bg-surface ring-ring-app/10 ${dialogClass}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Common/Modal/index.tsx` at line 119, Replace the hardcoded
ring color in the Modal dialog's container className (the template string in
src/components/Common/Modal/index.tsx) with the semantic ring token used
elsewhere (change ring-gray-700 to ring-ring-app/10) so the dialog uses the
theme token consistently with components like DeviceItem and RequestItem; update
the className string around the existing `dialogClass` usage to reference
`ring-ring-app/10` instead of `ring-gray-700`.
src/components/Discover/HeroSlider/index.tsx (2)

194-209: Inline styles for animations work but could be extracted.

The inline <style> block defines keyframe animations that are recreated on each render. While functional, consider moving these to globals.css or a CSS module for better caching and separation of concerns.

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

In `@src/components/Discover/HeroSlider/index.tsx` around lines 194 - 209, The
inline <style> block inside the HeroSlider component recreates keyframes on each
render; move the keyframes (heroSlideIn, heroSlideOut) and the related classes
(.hero-item, .hero-item-in, .hero-item-out) out of index.tsx into a shared
stylesheet (e.g., globals.css or a CSS module) and import that stylesheet into
the component, then remove the <style> block so the component uses the persisted
CSS classes and animations instead of embedding them at render time.

68-80: Carousel interval runs even when component is hidden.

Per Context snippet 2, HeroSlider is mounted inside a hidden CSS class div in Discover/index.tsx. However, the useEffect still starts a 7-second setInterval and the SWR fetch still runs, consuming resources for a component that's never displayed in the default theme.

Consider either:

  1. Passing a prop to disable auto-rotation when hidden
  2. Using the theme context to conditionally enable the carousel
  3. Not mounting the component at all when not AMOLED
// Example: accept a disabled prop
const HeroSlider = ({ disabled = false }: { disabled?: boolean }) => {
  // ...
  useEffect(() => {
    if (disabled || items.length <= 1) return;
    startInterval();
    return () => { /* cleanup */ };
  }, [items.length, startInterval, disabled]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Discover/HeroSlider/index.tsx` around lines 68 - 80,
HeroSlider currently always starts the rotation interval (startInterval) and
triggers its fetch even when hidden; update the HeroSlider component to accept a
prop (e.g., disabled?: boolean) or read the theme/context and gate both the
interval and any SWR/data hooks: modify the useEffect that calls startInterval
to early-return when disabled or theme indicates hidden (if disabled ||
items.length <= 1) and also avoid invoking the data-fetching hook (or return
null) when disabled so no network/interval work runs when the component is
hidden; reference startInterval, intervalRef, and the useEffect that currently
depends on items.length/startInterval to implement these checks and cleanup.
src/components/Discover/DiscoverTv/index.tsx (1)

68-75: Consider lazy-loading the backdrop based on theme.

The AMOLED backdrop and associated useEffect run unconditionally on every page load, fetching images and maintaining state even when the default theme hides the element via CSS (display: none in globals.css).

For efficiency, consider gating this with the theme check, similar to ImageFader (Context snippet 1):

import useTheme from '@app/hooks/useTheme';
// ...
const { theme } = useTheme();
const isAmoled = theme === 'amoled-strix';

useEffect(() => {
  if (!isAmoled || pickedRef.current || !titles?.length) return;
  // ... rest of backdrop selection logic
}, [titles, isAmoled]);

// In render:
{isAmoled && backdropPath && (
  <div className="discover-tv-amoled-backdrop ...">

This avoids unnecessary image fetches and DOM nodes when the feature is hidden.

Also applies to: 86-102

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

In `@src/components/Discover/DiscoverTv/index.tsx` around lines 68 - 75, The
backdrop selection useEffect (the effect using pickedRef, titles,
setBackdropPath and backdropPath) should be gated by theme to avoid work when
the AMOLED backdrop is hidden: import and call useTheme to get theme, derive
isAmoled (theme === 'amoled-strix'), add isAmoled to the effect dependencies and
short-circuit if !isAmoled, and only render the backdrop DOM (the element that
uses backdropPath) when isAmoled && backdropPath; mirror the same change for the
other effect/block referenced around lines 86-102 so both selection and
rendering are lazy based on isAmoled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Layout/PullToRefresh/index.tsx`:
- Line 120: The PullToRefresh component's JSX contains hardcoded tailwind colors
in the class string (the fragment ending with "relative -top-28 h-9 w-9
rounded-full border-4 shadow-md shadow-black ring-1 border-gray-800
bg-surface-raised ring-gray-700") — replace the two hardcoded classes
`border-gray-800` and `ring-gray-700` with the project's semantic border/ring
tokens (for example use the semantic border token like `border-surface-border`
and a semantic ring token like `ring-surface-ring` or whatever token names your
design system exports) so the component uses theme tokens; update the className
in the PullToRefresh component accordingly and run a quick visual check in
dark/AMOLED themes.

In `@src/components/PersonDetails/index.tsx`:
- Around line 245-269: The two ImageFader instances in PersonDetails (inside the
person-fader-standard and person-fader-amoled divs) mount simultaneously causing
duplicate image loads and duplicate rotation intervals; fix by mounting a single
ImageFader: compute the backgroundImages once from sortedCast and sortedCrew,
render only one ImageFader (referencing ImageFader and the computed
backgroundImages array), and control the visual differences (the AMOLED gradient
overlay or darker styling) via CSS or a prop (e.g., isAmoled/isDarker) so only
one useEffect/interval runs—remove the duplicate ImageFader and move the extra
gradient div or conditional styling to overlay the single fader instead.

In `@src/pages/users/`[userId]/settings/appearance.tsx:
- Around line 5-11: Add the same route guard used by other user settings pages:
call useRouteGuard(Permission.MANAGE_USERS) at the top of the
UserAppearanceSettingsPage component (import useRouteGuard and Permission) so
only users with MANAGE_USERS can access /users/[userId]/settings/appearance;
alternatively, if you prefer the UX change, detect when router.query.userId !==
currentUser.id and redirect to /profile/settings/appearance (use router.push)
instead of rendering UserAppearanceSettings—update imports and ensure this logic
lives inside UserAppearanceSettingsPage before returning the JSX.

---

Outside diff comments:
In `@src/components/Settings/OverrideRule/OverrideRuleTiles.tsx`:
- Around line 155-163: The inner .map() iterations inside the OverrideRuleTiles
component render spans without key props (e.g., the users map over rule.users,
genres map, languages map, keywords map, and tags map), causing unstable
reconciliation; update each mapped element (the span or fragment returned in the
users.map, genres.map, languages.map, keywords.map, and tags.map callbacks) to
include a stable key prop derived from the item (use userId or the mapped
id/string such as Number(userId), genre id/name, language code/id, keyword
id/text, or tag id) so React can track them reliably (ensure keys are unique
within each list and use the parsed Number(userId) or the original identifier as
appropriate).

---

Duplicate comments:
In `@src/components/Common/Tag/index.tsx`:
- Line 11: The Tag component's root div (in src/components/Common/Tag/index.tsx
— the inline-flex cursor-pointer items-center rounded-full ... element) uses
fixed palette classes (ring-gray-600 and hover:bg-gray-700) which break theming;
replace those fixed classes with the project's semantic tokens (e.g., use the
theme's ring and hover background tokens such as ring-<semantic-token> and
hover:bg-<semantic-token> — e.g., ring-border/ ring-muted and
hover:bg-surface-hover or the equivalent tokens your design system defines) so
the ring and hover states adapt to AMOLED-Strix and other themes while keeping
the rest of the class list intact.

In `@src/components/Discover/HeroSlider/index.tsx`:
- Around line 239-244: The "View Details" text in the Link inside the HeroSlider
is hardcoded; add a message key (e.g., viewDetails) to the component's messages
definition (use defineMessages with namespace 'components.Discover.HeroSlider')
and replace the literal "View Details" with
intl.formatMessage(messages.viewDetails) in the Link (the element using
href={detailUrl}); this aligns with existing i18n usage in the component.

In `@src/components/MediaSlider/index.tsx`:
- Line 192: Replace the hardcoded "See all" string in the MediaSlider component
with a localized message: import and use your app's i18n helper (e.g.,
useTranslation from 'react-i18next' or the project's t() utility) inside the
MediaSlider component and replace the literal "See all" JSX text with
t('mediaSlider.seeAll') (or the project's chosen key); also add the
'mediaSlider.seeAll' entry to the locale resource files for supported languages
so translations are available.

In `@src/components/MovieDetails/index.tsx`:
- Line 1411: Add localized labels for the AMOLED headings by extending the
existing messages object (identifier: messages) with keys crew, keywords,
ratings, and details and then replace the hardcoded strings "Crew", "Keywords",
"Ratings", and "Details" in the JSX with intl.formatMessage(messages.crew),
intl.formatMessage(messages.keywords), intl.formatMessage(messages.ratings), and
intl.formatMessage(messages.details) respectively; update usages near the h3
headings (the elements rendering those uppercase labels) so they pull from
intl.formatMessage instead of literal strings.
- Around line 1136-1139: The JSX contains a stray ternary closing token ") : ("
in the MovieDetails component causing a syntax error; fix it by introducing a
proper conditional around the two layout branches (standard vs AMOLED) — e.g.,
get theme from useTheme() (const { theme } = useTheme()) and compute isAmoled
(theme === 'amoled-strix'), then replace the fragment-wrapped pair with a
ternary expression using isAmoled ? <div className="relative bg-black">…</div> :
<div className="movie-details-backdrop-section …">…</div> (or invert the order
to match intended UX) so the opening condition matches the existing ") : (" and
removes the stray token.

In `@src/components/TitleCard/index.tsx`:
- Around line 510-543: Two h1 elements in the TitleCard component share the same
data-testid ("title-card-title") causing conflicting test queries; update them
to distinct values (for example change the AMOLED/first h1 to
"title-card-title-amoled" and the standard/second h1 inside the
title-card-standard-title-block to "title-card-title-standard") by locating the
h1 with data-testid in the AMOLED title block and the h1 inside the element with
className "title-card-standard-title-block", change their data-testid strings
accordingly, and then update any tests that reference the old test id to use the
new specific ids.
- Around line 377-416: The AMOLED icon-only <button> elements for removing from
watchlist and for blocklist/hide lack accessible names; update the two plain
<button> elements that call onClickDeleteWatchlistBtn and setShowBlocklistModal
(the ones with className "title-card-amoled-icon-btn") to include type="button"
and a descriptive aria-label (e.g., aria-label="Remove from watchlist" for
onClickDeleteWatchlistBtn and aria-label="Hide title" or "Block title" for
setShowBlocklistModal) so screen readers can announce their purpose; keep the
existing visual content and handlers unchanged.

In `@src/components/TvDetails/index.tsx`:
- Around line 360-362: The JSX currently has a broken ternary in the TvDetails
component: replace the stray `) : (` branch switch with a proper conditional
using a boolean (e.g., isAmoled) so the component renders either the standard
layout or the AMOLED layout (turn the fragment split into isAmoled ? (standard
JSX) : (amoled JSX)); ensure the boolean isAmoled is defined/derived in
TvDetails (or passed as a prop) and use it in the ternary, and convert the bare
comment "AMOLED full-bleed layout" into a JSX comment wrapped in braces `{/*
AMOLED full-bleed layout */}` so the file parses correctly.
- Line 1670: The hardcoded section headings ("Crew", "Keywords", "Ratings",
"Details") in the TvDetails component should be replaced with localized
messages; add message identifiers (e.g.
tvDetails.crew/tvDetails.keywords/tvDetails.ratings/tvDetails.details) to the
messages file and use intl.formatMessage({ id: 'tvDetails.crew', defaultMessage:
'Crew' }) (or equivalent ids/defaults) in the TvDetails component where the h3
headings are rendered (ensure the component has access to the intl object via
useIntl or injectIntl). Update the message definitions for those four keys and
replace the literal strings with intl.formatMessage calls so the headings are
translatable.

In `@src/styles/globals.css`:
- Around line 657-663: The CSS rule for the selectors
.collection-backdrop-overlay and .issue-backdrop-overlay uses Tailwind's
theme('colors.gray.900') which won't be resolved in plain CSS; replace that call
with a concrete color value or a CSS variable (for example the equivalent
rgba/hex for gray.900 or var(--color-gray-900)) and ensure --tw-gradient-to is
set if you want theme-aware behavior so the background-image uses a valid color
instead of theme('colors.gray.900').

---

Nitpick comments:
In `@src/components/Common/Modal/index.tsx`:
- Line 119: Replace the hardcoded ring color in the Modal dialog's container
className (the template string in src/components/Common/Modal/index.tsx) with
the semantic ring token used elsewhere (change ring-gray-700 to
ring-ring-app/10) so the dialog uses the theme token consistently with
components like DeviceItem and RequestItem; update the className string around
the existing `dialogClass` usage to reference `ring-ring-app/10` instead of
`ring-gray-700`.

In `@src/components/Common/SlideOver/index.tsx`:
- Line 74: The div in the SlideOver component currently hardcodes the tailwind
class ring-gray-700; update the class list in the JSX element (the div with
className starting "flex h-full flex-col rounded-lg shadow-xl ring-1
backdrop-blur bg-surface") to replace ring-gray-700 with the semantic token
ring-ring-app/10 so it matches other components (e.g., OverrideRuleTiles,
SettingsMetadata) and keeps styling consistent.

In `@src/components/Common/Tooltip/index.tsx`:
- Line 28: The tooltip class string currently hardcodes "border-gray-600";
update the Tooltip component's className string (the template literal that
includes "z-50 text-sm absolute font-normal px-2 py-1 rounded border shadow
text-gray-100 bg-surface-raised border-gray-600") to use the semantic border
utility (for example replace "border-gray-600" with a theme token class such as
"border-surface-border") so the tooltip border follows theme token remapping.

In `@src/components/Discover/DiscoverTv/index.tsx`:
- Around line 68-75: The backdrop selection useEffect (the effect using
pickedRef, titles, setBackdropPath and backdropPath) should be gated by theme to
avoid work when the AMOLED backdrop is hidden: import and call useTheme to get
theme, derive isAmoled (theme === 'amoled-strix'), add isAmoled to the effect
dependencies and short-circuit if !isAmoled, and only render the backdrop DOM
(the element that uses backdropPath) when isAmoled && backdropPath; mirror the
same change for the other effect/block referenced around lines 86-102 so both
selection and rendering are lazy based on isAmoled.

In `@src/components/Discover/HeroSlider/index.tsx`:
- Around line 194-209: The inline <style> block inside the HeroSlider component
recreates keyframes on each render; move the keyframes (heroSlideIn,
heroSlideOut) and the related classes (.hero-item, .hero-item-in,
.hero-item-out) out of index.tsx into a shared stylesheet (e.g., globals.css or
a CSS module) and import that stylesheet into the component, then remove the
<style> block so the component uses the persisted CSS classes and animations
instead of embedding them at render time.
- Around line 68-80: HeroSlider currently always starts the rotation interval
(startInterval) and triggers its fetch even when hidden; update the HeroSlider
component to accept a prop (e.g., disabled?: boolean) or read the theme/context
and gate both the interval and any SWR/data hooks: modify the useEffect that
calls startInterval to early-return when disabled or theme indicates hidden (if
disabled || items.length <= 1) and also avoid invoking the data-fetching hook
(or return null) when disabled so no network/interval work runs when the
component is hidden; reference startInterval, intervalRef, and the useEffect
that currently depends on items.length/startInterval to implement these checks
and cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 780101fd-ffec-45a5-8d43-7cfb9182cb1a

📥 Commits

Reviewing files that changed from the base of the PR and between e1b55e6 and 56a92b0.

📒 Files selected for processing (71)
  • .github/workflows/release.yml
  • src/components/Blocklist/index.tsx
  • src/components/CollectionDetails/index.tsx
  • src/components/Common/ButtonWithDropdown/index.tsx
  • src/components/Common/Dropdown/index.tsx
  • src/components/Common/ImageFader/index.tsx
  • src/components/Common/Modal/index.tsx
  • src/components/Common/PlayButton/index.tsx
  • src/components/Common/SettingsTabs/index.tsx
  • src/components/Common/SlideOver/index.tsx
  • src/components/Common/Table/index.tsx
  • src/components/Common/Tag/index.tsx
  • src/components/Common/Tooltip/index.tsx
  • src/components/Discover/DiscoverMovies/index.tsx
  • src/components/Discover/DiscoverSliderEdit/index.tsx
  • src/components/Discover/DiscoverTv/index.tsx
  • src/components/Discover/HeroSlider/index.tsx
  • src/components/Discover/Trending.tsx
  • src/components/Discover/index.tsx
  • src/components/IssueDetails/IssueComment/index.tsx
  • src/components/IssueDetails/index.tsx
  • src/components/IssueList/IssueItem/index.tsx
  • src/components/IssueList/index.tsx
  • src/components/IssueModal/CreateIssueModal/index.tsx
  • src/components/Layout/AmoledNavbar/index.tsx
  • src/components/Layout/PullToRefresh/index.tsx
  • src/components/Layout/UserDropdown/index.tsx
  • src/components/Layout/index.tsx
  • src/components/Login/JellyfinLogin.tsx
  • src/components/Login/LocalLogin.tsx
  • src/components/Login/index.tsx
  • src/components/MediaSlider/index.tsx
  • src/components/MovieDetails/index.tsx
  • src/components/PersonDetails/index.tsx
  • src/components/RegionSelector/index.tsx
  • src/components/RequestBlock/index.tsx
  • src/components/RequestButton/index.tsx
  • src/components/RequestCard/index.tsx
  • src/components/RequestList/RequestItem/index.tsx
  • src/components/RequestList/index.tsx
  • src/components/RequestModal/AdvancedRequester/index.tsx
  • src/components/ResetPassword/RequestResetLink.tsx
  • src/components/ResetPassword/index.tsx
  • src/components/Settings/OverrideRule/OverrideRuleTiles.tsx
  • src/components/Settings/SettingsAbout/Releases/index.tsx
  • src/components/Settings/SettingsJellyfin.tsx
  • src/components/Settings/SettingsLogs/index.tsx
  • src/components/Settings/SettingsMetadata.tsx
  • src/components/Settings/SettingsPlex.tsx
  • src/components/Settings/SettingsServices.tsx
  • src/components/Setup/JellyfinSetup.tsx
  • src/components/Setup/index.tsx
  • src/components/Slider/index.tsx
  • src/components/TitleCard/ErrorCard.tsx
  • src/components/TitleCard/index.tsx
  • src/components/Toast/index.tsx
  • src/components/TvDetails/Season/index.tsx
  • src/components/TvDetails/index.tsx
  • src/components/UserList/index.tsx
  • src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem.tsx
  • src/components/UserProfile/UserSettings/index.tsx
  • src/components/UserProfile/index.tsx
  • src/context/ThemeContext.tsx
  • src/hooks/useTheme.ts
  • src/pages/_app.tsx
  • src/pages/profile/settings/appearance.tsx
  • src/pages/users/[userId]/settings/appearance.tsx
  • src/styles/globals.css
  • tailwind.config.js
✅ Files skipped from review due to trivial changes (1)
  • src/components/Setup/JellyfinSetup.tsx
🚧 Files skipped from review as they are similar to previous changes (32)
  • src/components/Settings/SettingsJellyfin.tsx
  • src/components/Discover/index.tsx
  • src/components/UserProfile/index.tsx
  • src/components/Toast/index.tsx
  • src/components/IssueList/index.tsx
  • src/components/Settings/SettingsLogs/index.tsx
  • src/components/RequestCard/index.tsx
  • src/components/RequestBlock/index.tsx
  • src/components/Common/Table/index.tsx
  • src/components/RequestModal/AdvancedRequester/index.tsx
  • src/components/RequestList/index.tsx
  • src/components/Discover/DiscoverSliderEdit/index.tsx
  • src/components/ResetPassword/RequestResetLink.tsx
  • src/hooks/useTheme.ts
  • src/pages/_app.tsx
  • src/components/IssueDetails/IssueComment/index.tsx
  • src/components/Blocklist/index.tsx
  • src/components/Settings/SettingsServices.tsx
  • src/components/Login/LocalLogin.tsx
  • src/components/Login/index.tsx
  • src/components/TitleCard/ErrorCard.tsx
  • src/components/Layout/AmoledNavbar/index.tsx
  • src/components/RegionSelector/index.tsx
  • src/components/Layout/UserDropdown/index.tsx
  • src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx
  • src/components/UserList/index.tsx
  • src/components/Slider/index.tsx
  • src/components/Settings/SettingsPlex.tsx
  • src/pages/profile/settings/appearance.tsx
  • src/components/Discover/DiscoverMovies/index.tsx
  • src/components/ResetPassword/index.tsx
  • src/context/ThemeContext.tsx

Comment thread src/components/Layout/PullToRefresh/index.tsx Outdated
Comment thread src/components/PersonDetails/index.tsx Outdated
Comment thread src/pages/users/[userId]/settings/appearance.tsx
@RAFSuNX
Copy link
Copy Markdown
Author

RAFSuNX commented Mar 14, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
src/styles/globals.css (1)

660-663: ⚠️ Potential issue | 🟡 Minor

Fix Stylelint spacing violation in overlay gradient rule.

Line 662 triggers declaration-empty-line-before; this can fail style checks.

Suggested fix
 [data-theme='amoled-strix'] .collection-backdrop-overlay,
 [data-theme='amoled-strix'] .issue-backdrop-overlay {
   --tw-gradient-from: rgba(0, 0, 0, 0.6);
   --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
+
   background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgb(17, 24, 39));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/styles/globals.css` around lines 660 - 663, The stylelint
`declaration-empty-line-before` violation is triggered by the
`--tw-gradient-stops` declaration; fix it by adding a single empty line
immediately before the `--tw-gradient-stops` line in the same CSS block so the
declaration has the required blank line separation (adjust the block containing
`--tw-gradient-from`, `--tw-gradient-stops`, and `background-image`).
src/components/TitleCard/index.tsx (1)

377-414: ⚠️ Potential issue | 🟠 Major

Add accessible names and explicit button type to icon-only action controls.

Line 377, Line 390, and Line 406 (<button>) plus Line 383, Line 395, and Line 412 (<Button>) are icon-only and still have no accessible label; they should also use type="button" explicitly.

♿ Suggested fix
+// messages
+removeFromWatchList: 'Remove from watchlist',
+addToBlockList: 'Add to blocklist',

-<button
+<button
+  type="button"
+  aria-label={intl.formatMessage(messages.addToWatchList)}
   className="title-card-amoled-icon-btn ..."
   onClick={onClickWatchlistBtn}
>

-<Button buttonType={'ghost'} className="title-card-standard-icon-btn z-40" buttonSize={'sm'} onClick={onClickWatchlistBtn}>
+<Button
+  buttonType="ghost"
+  aria-label={intl.formatMessage(messages.addToWatchList)}
+  className="title-card-standard-icon-btn z-40"
+  buttonSize="sm"
+  onClick={onClickWatchlistBtn}
+>

-<button
+<button
+  type="button"
+  aria-label={intl.formatMessage(messages.removeFromWatchList)}
   className="title-card-amoled-icon-btn ..."
   onClick={onClickDeleteWatchlistBtn}
>

-<Button className="title-card-standard-icon-btn z-40" buttonSize={'sm'} onClick={onClickDeleteWatchlistBtn}>
+<Button
+  buttonType="ghost"
+  aria-label={intl.formatMessage(messages.removeFromWatchList)}
+  className="title-card-standard-icon-btn z-40"
+  buttonSize="sm"
+  onClick={onClickDeleteWatchlistBtn}
+>

-<button
+<button
+  type="button"
+  aria-label={intl.formatMessage(messages.addToBlockList)}
   className="title-card-amoled-icon-btn ..."
   onClick={() => setShowBlocklistModal(true)}
>

-<Button buttonType={'ghost'} className="title-card-standard-icon-btn z-40" buttonSize={'sm'} onClick={() => setShowBlocklistModal(true)}>
+<Button
+  buttonType="ghost"
+  aria-label={intl.formatMessage(messages.addToBlockList)}
+  className="title-card-standard-icon-btn z-40"
+  buttonSize="sm"
+  onClick={() => setShowBlocklistModal(true)}
+>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TitleCard/index.tsx` around lines 377 - 414, The icon-only
native <button> elements and the custom <Button> instances in TitleCard (the
ones rendering StarIcon, MinusCircleIcon, and EyeSlashIcon) need explicit
type="button" and accessible labels; update each native <button> (e.g., the two
StarIcon/MinusCircleIcon/EyeSlashIcon <button> tags) to include type="button"
and aria-label attributes (e.g., aria-label="Add to watchlist", "Remove from
watchlist", "Hide title"), and pass a type="button" prop plus an aria-label prop
to the corresponding <Button> components (or wrap a visually-hidden text label)
so screen readers can announce the action while keeping the existing onClick
handlers (onClickWatchlistBtn, onClickDeleteWatchlistBtn,
setShowBlocklistModal).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/TitleCard/index.tsx`:
- Around line 377-414: The icon-only native <button> elements and the custom
<Button> instances in TitleCard (the ones rendering StarIcon, MinusCircleIcon,
and EyeSlashIcon) need explicit type="button" and accessible labels; update each
native <button> (e.g., the two StarIcon/MinusCircleIcon/EyeSlashIcon <button>
tags) to include type="button" and aria-label attributes (e.g., aria-label="Add
to watchlist", "Remove from watchlist", "Hide title"), and pass a type="button"
prop plus an aria-label prop to the corresponding <Button> components (or wrap a
visually-hidden text label) so screen readers can announce the action while
keeping the existing onClick handlers (onClickWatchlistBtn,
onClickDeleteWatchlistBtn, setShowBlocklistModal).

In `@src/styles/globals.css`:
- Around line 660-663: The stylelint `declaration-empty-line-before` violation
is triggered by the `--tw-gradient-stops` declaration; fix it by adding a single
empty line immediately before the `--tw-gradient-stops` line in the same CSS
block so the declaration has the required blank line separation (adjust the
block containing `--tw-gradient-from`, `--tw-gradient-stops`, and
`background-image`).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff508d56-648b-4b2a-95fb-c35a4a3a1c66

📥 Commits

Reviewing files that changed from the base of the PR and between 56a92b0 and 1b190e9.

📒 Files selected for processing (9)
  • src/components/Common/Tag/index.tsx
  • src/components/Discover/Trending.tsx
  • src/components/IssueModal/CreateIssueModal/index.tsx
  • src/components/Layout/PullToRefresh/index.tsx
  • src/components/Login/JellyfinLogin.tsx
  • src/components/Login/LocalLogin.tsx
  • src/components/Settings/OverrideRule/OverrideRuleTiles.tsx
  • src/components/TitleCard/index.tsx
  • src/styles/globals.css
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/Layout/PullToRefresh/index.tsx
  • src/components/Login/LocalLogin.tsx
  • src/components/IssueModal/CreateIssueModal/index.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Discover/HeroSlider/index.tsx`:
- Around line 74-79: The modal opens while the slider auto-rotates so
RequestModal reads mutable featured props and can request/apply status to the
wrong slide; fix by capturing and passing a frozen target into the modal when
opening. Specifically, when opening the RequestModal (the handler that currently
reads featured.tmdbId/type), create and store a frozen object (e.g.,
selectedRequestTarget with tmdbId and type) and pass that into RequestModal
instead of letting it read from featured; also set a flag (e.g.,
isRequestModalOpen) so startInterval/next/intervalRef/isPaused respect it (pause
rotation or prevent featured updates) while the modal is open. Apply the same
change to the other modal-opening handlers referenced (the blocks around lines
143-153 and 251-259) so all modal openings use the frozen selectedRequestTarget
rather than the live featured value.
- Around line 81-90: When items changes it can become shorter than the existing
currentIndex, so accessing items[currentIndex] may be undefined; update the
component to clamp or reset currentIndex whenever items.length changes (e.g., in
the existing useEffect or a new useEffect that depends on items.length) so
currentIndex = Math.min(currentIndex, Math.max(0, items.length - 1)) or set to 0
when items.length === 0, and also guard any subsequent accesses (e.g., where
`featured = items[currentIndex]` and the rendering block around that and the
code referenced at lines 95-101) by checking that featured is defined before
reading its properties or rendering its UI.
- Around line 156-159: The backdrop stack keys use only item.id which can
collide between movies and TV shows; update the key in the items.map render (in
the HeroSlider component) to use a composite key including the media type and id
(e.g., combine item.type or item.media_type with item.id, with a fallback to the
map index if type is missing) so each element has a stable, unique key across
mixed media.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7713591a-c43f-44d3-9f35-f2ecd8dc4f7e

📥 Commits

Reviewing files that changed from the base of the PR and between 1b190e9 and c9db5b3.

📒 Files selected for processing (2)
  • src/components/Discover/HeroSlider/index.tsx
  • src/components/MediaSlider/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/MediaSlider/index.tsx

Comment thread src/components/Discover/HeroSlider/index.tsx
Comment thread src/components/Discover/HeroSlider/index.tsx
Comment thread src/components/Discover/HeroSlider/index.tsx
@RAFSuNX
Copy link
Copy Markdown
Author

RAFSuNX commented Mar 14, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/components/MovieDetails/index.tsx (1)

1140-1143: ⚠️ Potential issue | 🔴 Critical

Critical: Invalid JSX — broken ternary with missing condition.

The return block opens with a fragment <> at line 438, but line 1141 contains ) : ( which is the else-branch of a ternary operator without a corresponding opening condition. This is a syntax error that will prevent the code from compiling.

Additionally:

  • useTheme hook is not imported
  • No condition variable (e.g., isAmoled) is defined to gate the layout branches
  • Line 1142's comment must use JSX syntax: {/* comment */} (flagged by Biome)

To fix, import the theme hook and add a proper conditional:

🐛 Proposed structural fix
 import useSettings from '@app/hooks/useSettings';
+import useTheme from '@app/hooks/useTheme';
 import { Permission, UserType, useUser } from '@app/hooks/useUser';

Then at the start of the component:

 const MovieDetails = ({ movie }: MovieDetailsProps) => {
   const settings = useSettings();
+  const { theme } = useTheme();
+  const isAmoled = theme === 'amoled-strix';
   const { user, hasPermission } = useUser();

Then restructure the return statement:

   return (
-    <>
-      <div
-        className="movie-details-backdrop-section media-page"
+    isAmoled ? (
+      {/* ── AMOLED full-bleed layout ── */}
+      <div className="relative bg-black">
         ...
       </div>
-      ) : (
-      /* ── AMOLED full-bleed layout ── */
-      <div className="relative bg-black">
+    ) : (
+      <div
+        className="movie-details-backdrop-section media-page"
         ...
       </div>
-    </>
+    )
   );

,

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

In `@src/components/MovieDetails/index.tsx` around lines 1140 - 1143, The JSX
currently has a broken ternary in the MovieDetails component (the " ) : ( " else
branch has no opening condition) and is missing the theme hook and a condition
variable; fix by importing useTheme and inside the MovieDetails component derive
a boolean (e.g., isAmoled) from useTheme (or theme.mode) and use that boolean as
the ternary condition that chooses between the two layouts, ensure the top-level
fragment/return wraps the full expression so the ternary is complete, and
replace the HTML comment on line 1142 with a JSX comment syntax {/* ... */} so
the JSX compiles.
src/components/TvDetails/index.tsx (1)

1364-1365: ⚠️ Potential issue | 🔴 Critical

Critical: Broken ternary expression makes this component unparseable.

Line 1364 contains ) : ( which is the else branch of a ternary operator, but there is no opening ? ( before it. The standard layout (lines 470-1363) is not wrapped in any conditional expression. This code will fail to compile.

Additionally, the comment on line 1365 is not wrapped in braces, which Biome correctly flags.

To fix this, you need to:

  1. Add a theme conditional with useTheme hook and wrap the two layouts in a proper ternary: isAmoled ? (AMOLED layout) : (Standard layout)
  2. OR remove the stray ) : ( entirely if both layouts should be CSS-controlled via [data-theme] selectors
🐛 Proposed fix using theme conditional
+import { useTheme } from '@app/context/ThemeContext';
 // ... other imports
 
 const TvDetails = ({ tv }: TvDetailsProps) => {
+  const { theme } = useTheme();
+  const isAmoled = theme === 'amoled-strix';
   const settings = useSettings();
   // ... rest of component
 
   return (
     <>
+    {isAmoled ? (
+      {/* ── AMOLED full-bleed layout ── */}
+      <div className="relative bg-black">
+        {/* ... AMOLED content ... */}
+      </div>
+    ) : (
       <div className="tv-details-backdrop-section media-page" /* ... */>
         {/* ... Standard layout content ... */}
       </div>
-    ) : (
-    /* ── AMOLED full-bleed layout ── */
-    <div className="relative bg-black">
-      {/* ... */}
-    </div>
+    )}
     </>
   );
 };

Run the following script to verify the JSX structure and check if useTheme is available:

#!/bin/bash
# Check if the component currently imports useTheme
rg -n "useTheme" src/components/TvDetails/index.tsx

# Check if ThemeContext exists and exports useTheme
fd -t f "ThemeContext" --exec cat {} 2>/dev/null | head -50

# Check for any isAmoled variable in the component
rg -n "isAmoled" src/components/TvDetails/index.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TvDetails/index.tsx` around lines 1364 - 1365, The JSX
contains a stray "):(" else-branch and an unbraced comment making the component
unparsable; fix it by importing and using the theme hook (e.g., import {
useTheme } from ThemeContext), derive a boolean (e.g., const isAmoled =
useTheme().name === 'amoled' or similar) inside the TvDetails component, and
wrap the two layout fragments in a proper ternary like isAmoled ? (/* AMOLED
full-bleed layout */ ...) : (/* Standard layout */ ...); alternatively, if theme
switching is handled by CSS, remove the stray ") : (" and ensure the comment is
inside JSX braces or moved outside JSX so the file parses (references: TvDetails
component, useTheme, isAmoled).
🧹 Nitpick comments (3)
src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx (1)

110-111: Localize theme name/description instead of rendering raw literals.

Line 110 and Line 111 bypass react-intl, so these strings remain English in localized UIs. Consider mapping theme IDs to localized messages (or storing message keys in theme definitions).

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

In `@src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx`
around lines 110 - 111, UserAppearanceSettings currently renders theme strings
directly via t.name and t.description, bypassing react-intl; replace those raw
literals with localized messages by mapping each theme ID (or add a message key
to the theme definition) to an intl message descriptor and render via
formatMessage or <FormattedMessage>. Locate usages of t.name and t.description
in the UserAppearanceSettings component and swap them to look up the correct
message descriptor (e.g., themeLabels[theme.id] or theme.messageId) and call
intl.formatMessage(messageDescriptor) (or <FormattedMessage id={...}
defaultMessage={...} />) so names and descriptions are localized. Ensure you add
corresponding message IDs to your messages file and fall back to the original
string if no message is found.
src/components/Layout/AmoledNavbar/index.tsx (1)

100-108: Consider moving keyframe animation to globals.css.

The inline <style> tag is recreated on every render. While this works, moving the @keyframes iconPop definition to globals.css would be slightly more efficient and keep the component JSX cleaner.

♻️ Proposed refactor

Add to src/styles/globals.css:

`@keyframes` iconPop {
  from { opacity: 0; transform: scale(0.6) rotate(-15deg); }
  to   { opacity: 1; transform: scale(1) rotate(0deg); }
}

.icon-pop {
  animation: iconPop 250ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

Then remove lines 100-108 from the component.

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

In `@src/components/Layout/AmoledNavbar/index.tsx` around lines 100 - 108, Move
the inline keyframe and animation CSS out of the AmoledNavbar component: remove
the <style> block that defines `@keyframes` iconPop and .icon-pop from
src/components/Layout/AmoledNavbar/index.tsx and instead add those rules to your
global stylesheet (e.g., src/styles/globals.css). Keep the .icon-pop class usage
in the JSX unchanged so elements still use the animation, but ensure `@keyframes`
iconPop and the .icon-pop rule live in globals.css to avoid recreating the style
tag on every render.
src/components/TvDetails/index.tsx (1)

1901-1962: Consider extracting shared season logic to reduce duplication.

The season status computation logic (lines 1914-1958) is identical to the standard layout (lines 793-838). This duplication spans ~150 lines of complex request/season filtering that will need to be maintained in sync.

Consider extracting the shared logic into helper functions or a custom hook to improve maintainability.

♻️ Example extraction approach
// Could be extracted to a helper or hook
function useSeasonStatus(data: TvDetailsType, seasonNumber: number, is4k: boolean) {
  const mediaSeasons = data.mediaInfo?.seasons ?? [];
  const requests = data.mediaInfo?.requests ?? [];
  
  const mSeason = mediaSeasons.find(
    (s) => s.seasonNumber === seasonNumber && 
           s[is4k ? 'status4k' : 'status'] !== MediaStatus.UNKNOWN
  );
  
  const request = requests
    .filter((r) => 
      r.seasons.some((s) => s.seasonNumber === seasonNumber) && 
      r.is4k === is4k
    )
    .sort((a, b) => 
      new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
    )[0];
    
  return { mSeason, request };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TvDetails/index.tsx` around lines 1901 - 1962, The season
status/request computation duplicated in TvDetails (variables mSeason,
mSeason4k, request, request4k and show4k logic) should be extracted into a
reusable helper or hook (e.g. useSeasonStatus or getSeasonRequests) that accepts
data (TvDetailsType), seasonNumber and is4k and returns the computed mSeason and
request; replace the repeated blocks in both the expanded-season layout (current
block) and the standard layout (the earlier block that sets mSeason/request)
with calls to that helper, and keep the show4k permission check in the component
but pass is4k when calling the helper for 4k vs non-4k results.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx`:
- Around line 47-54: The theme buttons currently remove the default focus
outline and only show a visual selected state; restore keyboard focus visibility
and expose selection to assistive tech by adding focus-visible styling and
semantic pressed state: update the button rendered in UserAppearanceSettings
(the element that calls setTheme(t.id)) to remove the bare focus-outline removal
and instead include a visible focus-visible class (e.g., ring or outline on
focus-visible and a clear focus transition) and add aria-pressed={isActive} (or
aria-selected if part of a listbox) so screen readers know which theme is
active; apply the same changes to the other theme button block (the one around
lines 88-101) so both keyboard focus and pressed semantics are present.

---

Duplicate comments:
In `@src/components/MovieDetails/index.tsx`:
- Around line 1140-1143: The JSX currently has a broken ternary in the
MovieDetails component (the " ) : ( " else branch has no opening condition) and
is missing the theme hook and a condition variable; fix by importing useTheme
and inside the MovieDetails component derive a boolean (e.g., isAmoled) from
useTheme (or theme.mode) and use that boolean as the ternary condition that
chooses between the two layouts, ensure the top-level fragment/return wraps the
full expression so the ternary is complete, and replace the HTML comment on line
1142 with a JSX comment syntax {/* ... */} so the JSX compiles.

In `@src/components/TvDetails/index.tsx`:
- Around line 1364-1365: The JSX contains a stray "):(" else-branch and an
unbraced comment making the component unparsable; fix it by importing and using
the theme hook (e.g., import { useTheme } from ThemeContext), derive a boolean
(e.g., const isAmoled = useTheme().name === 'amoled' or similar) inside the
TvDetails component, and wrap the two layout fragments in a proper ternary like
isAmoled ? (/* AMOLED full-bleed layout */ ...) : (/* Standard layout */ ...);
alternatively, if theme switching is handled by CSS, remove the stray ") : ("
and ensure the comment is inside JSX braces or moved outside JSX so the file
parses (references: TvDetails component, useTheme, isAmoled).

---

Nitpick comments:
In `@src/components/Layout/AmoledNavbar/index.tsx`:
- Around line 100-108: Move the inline keyframe and animation CSS out of the
AmoledNavbar component: remove the <style> block that defines `@keyframes` iconPop
and .icon-pop from src/components/Layout/AmoledNavbar/index.tsx and instead add
those rules to your global stylesheet (e.g., src/styles/globals.css). Keep the
.icon-pop class usage in the JSX unchanged so elements still use the animation,
but ensure `@keyframes` iconPop and the .icon-pop rule live in globals.css to
avoid recreating the style tag on every render.

In `@src/components/TvDetails/index.tsx`:
- Around line 1901-1962: The season status/request computation duplicated in
TvDetails (variables mSeason, mSeason4k, request, request4k and show4k logic)
should be extracted into a reusable helper or hook (e.g. useSeasonStatus or
getSeasonRequests) that accepts data (TvDetailsType), seasonNumber and is4k and
returns the computed mSeason and request; replace the repeated blocks in both
the expanded-season layout (current block) and the standard layout (the earlier
block that sets mSeason/request) with calls to that helper, and keep the show4k
permission check in the component but pass is4k when calling the helper for 4k
vs non-4k results.

In `@src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx`:
- Around line 110-111: UserAppearanceSettings currently renders theme strings
directly via t.name and t.description, bypassing react-intl; replace those raw
literals with localized messages by mapping each theme ID (or add a message key
to the theme definition) to an intl message descriptor and render via
formatMessage or <FormattedMessage>. Locate usages of t.name and t.description
in the UserAppearanceSettings component and swap them to look up the correct
message descriptor (e.g., themeLabels[theme.id] or theme.messageId) and call
intl.formatMessage(messageDescriptor) (or <FormattedMessage id={...}
defaultMessage={...} />) so names and descriptions are localized. Ensure you add
corresponding message IDs to your messages file and fall back to the original
string if no message is found.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9a135842-0047-43bd-bf77-412cc2528fb1

📥 Commits

Reviewing files that changed from the base of the PR and between c9db5b3 and 59a7bae.

📒 Files selected for processing (5)
  • src/components/Layout/AmoledNavbar/index.tsx
  • src/components/MovieDetails/index.tsx
  • src/components/TvDetails/index.tsx
  • src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx
  • src/pages/users/[userId]/settings/appearance.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/pages/users/[userId]/settings/appearance.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

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 (6)
src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx (1)

47-55: ⚠️ Potential issue | 🟠 Major

Restore keyboard focus visibility and selected-state semantics on theme buttons.

Line 50 removes the focus outline without a visible replacement, and the active state is only visual. Please add explicit focus-visible styling and aria-pressed (and set type="button").

Suggested patch
               <button
                 key={t.id}
+                type="button"
                 onClick={() => setTheme(t.id)}
-                className={`relative flex flex-col overflow-hidden rounded-xl border-2 text-left transition duration-200 focus:outline-none ${
+                aria-pressed={isActive}
+                className={`relative flex flex-col overflow-hidden rounded-xl border-2 text-left transition duration-200 focus:outline-none ${
                   isActive
                     ? 'border-indigo-500 shadow-lg shadow-indigo-500/20'
                     : 'border-gray-700 hover:border-gray-500'
-                }`}
+                } focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-black`}
                 style={{ background: bgColor }}
               >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx`
around lines 47 - 55, The theme buttons in UserAppearanceSettings removed the
default focus outline and lack accessibility semantics; update the button
rendered in the map (the element with key={t.id} and onClick={() =>
setTheme(t.id)}) to include type="button", set aria-pressed={isActive} to
reflect selected state, and replace the removed focus outline with an explicit
visible focus-visible style (e.g. add focus-visible:ring and
focus-visible:ring-offset classes or similar accessible focus indicator
alongside any existing shadow/highlight classes) so keyboard users see focus.
src/components/MovieDetails/index.tsx (1)

437-440: ⚠️ Potential issue | 🔴 Critical

Fix the broken ternary operator structure — this code will not compile.

Same issue as TvDetails: the return block opens with a fragment <> at line 438, but line 1141 contains ) : ( which is the "else" branch of a ternary without a matching opening condition. This is invalid JSX.

🐛 Proposed structural fix
  return (
-   <>
-     <div
-       className="movie-details-backdrop-section media-page"
+   isAmoled ? (
+     /* ── AMOLED full-bleed layout ── */
+     <div className="relative bg-black">
+       {/* ... AMOLED content ... */}
+     </div>
+   ) : (
+     <div
+       className="movie-details-backdrop-section media-page"

You'll need to:

  1. Import the theme hook (e.g., useTheme from the new ThemeProvider)
  2. Add the condition variable (e.g., const { theme } = useTheme(); const isAmoled = theme === 'amoled-strix')
  3. Wrap each layout branch properly in the ternary
  4. Remove or restructure the fragment wrapper at line 1707

Also applies to: 1140-1143

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

In `@src/components/MovieDetails/index.tsx` around lines 437 - 440, The JSX return
in MovieDetails is broken because a fragment <> is left open while a ternary's
"else" branch `) : (` appears later (invalid JSX); fix by importing and using
the theme hook (e.g., useTheme) inside the MovieDetails component to declare the
condition (const { theme } = useTheme(); const isAmoled = theme ===
'amoled-strix'), then restructure the return so the ternary is complete and each
branch is wrapped in its own top-level element (remove the stray fragment
wrapper around the whole return and ensure the conditionally rendered branches
are returned as `isAmoled ? (<AmoledLayout ... />) : (<DefaultLayout ... />)` or
equivalent), and update any references to the previous fragment children to live
inside those branch elements so the JSX compiles.
src/components/TvDetails/index.tsx (1)

468-471: ⚠️ Potential issue | 🔴 Critical

Fix the broken ternary operator structure — this code will not compile.

The return block opens with a fragment <> at line 469, but line 1364 contains ) : ( which is the "else" branch of a ternary operator without a matching opening condition (condition ? (). This is invalid JSX syntax.

The code needs a conditional to switch between the standard and AMOLED layouts. Based on the PR description, this should likely use a theme check.

🐛 Proposed structural fix
  return (
-   <>
-   <div
-     className="tv-details-backdrop-section media-page"
+   isAmoled ? (
+     /* ── AMOLED full-bleed layout ── */
+     <div className="relative bg-black">
+       {/* ... AMOLED content ... */}
+     </div>
+   ) : (
+     <div
+       className="tv-details-backdrop-section media-page"

You'll need to:

  1. Import the theme hook (e.g., useTheme or check [data-theme='amoled-strix'])
  2. Add the condition variable (e.g., const isAmoled = theme === 'amoled-strix')
  3. Wrap each layout branch properly in the ternary
  4. Ensure the fragment <>...</> wrapping at line 2248 is removed or properly structured

Also applies to: 1364-1366

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

In `@src/components/TvDetails/index.tsx` around lines 468 - 471, The JSX return
contains an orphaned ternary "):(" else branch — fix by importing your theme
hook (e.g., useTheme or reading data-theme), create a boolean like const
isAmoled = theme === 'amoled-strix', and rewrite the top-level return to use
isAmoled ? ( /* AMOLED layout JSX branch */ ) : ( /* standard layout JSX branch
*/ ), removing the stray fragment wrappers so each branch is a valid JSX
expression; ensure the fragment opened at the original return is either removed
or encompassed correctly inside the chosen conditional and that any helper
render functions (if present) are called inside their respective branch.
src/components/Discover/HeroSlider/index.tsx (1)

157-161: ⚠️ Potential issue | 🟡 Minor

currentStatus may display on the wrong slide after modal completion.

The requestTarget correctly freezes the item being requested, but currentStatus is unconditionally set on completion. If the user starts a request, the slide auto-rotates to a different item, and then the request completes, currentStatus will be displayed on the new featured item rather than the one that was actually requested.

Consider verifying the target matches before updating the status:

 onComplete={(s) => {
+  if (
+    requestTarget?.id === featured.id &&
+    requestTarget?.type === (isMovie ? 'movie' : 'tv')
+  ) {
     setCurrentStatus(s);
+  }
   setShowRequestModal(false);
   setRequestTarget(null);
 }}

Alternatively, pause auto-rotation while the modal is open by setting isPaused.current = true when opening and resetting it on close.

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

In `@src/components/Discover/HeroSlider/index.tsx` around lines 157 - 161, The
onComplete handler unconditionally sets currentStatus which can appear on a
different slide if the carousel rotated after the request started; update the
onComplete handler in the component (the function handling onComplete where
setCurrentStatus, setShowRequestModal, setRequestTarget are called) to first
verify the requestTarget still matches the currently featured item (e.g.,
compare requestTarget.id or unique key to the active slide id) before calling
setCurrentStatus, or alternatively implement pause-until-complete behavior by
setting isPaused.current = true when opening the modal and restoring it on close
so the slide cannot auto-rotate while a request is in progress.
src/components/TitleCard/index.tsx (1)

385-387: ⚠️ Potential issue | 🟠 Major

Add accessible names to icon-only standard Button controls.

The AMOLED native buttons are labeled, but the standard icon-only Button variants are still unlabeled. In the standard theme, these controls may be announced as unnamed buttons.

♿ Suggested patch
-<Button buttonType={'ghost'} className="title-card-standard-icon-btn z-40" buttonSize={'sm'} onClick={onClickWatchlistBtn}>
+<Button
+  buttonType={'ghost'}
+  aria-label={intl.formatMessage(messages.addToWatchList)}
+  className="title-card-standard-icon-btn z-40"
+  buttonSize={'sm'}
+  onClick={onClickWatchlistBtn}
+>
   <StarIcon className={'h-3 text-amber-300'} />
 </Button>

-<Button className="title-card-standard-icon-btn z-40" buttonSize={'sm'} onClick={onClickDeleteWatchlistBtn}>
+<Button
+  aria-label={intl.formatMessage(messages.removeFromWatchList)}
+  className="title-card-standard-icon-btn z-40"
+  buttonSize={'sm'}
+  onClick={onClickDeleteWatchlistBtn}
+>
   <MinusCircleIcon className={'h-3'} />
 </Button>

-<Button buttonType={'ghost'} className="title-card-standard-icon-btn z-40" buttonSize={'sm'} onClick={() => setShowBlocklistModal(true)}>
+<Button
+  buttonType={'ghost'}
+  aria-label={intl.formatMessage(globalMessages.blocklist)}
+  className="title-card-standard-icon-btn z-40"
+  buttonSize={'sm'}
+  onClick={() => setShowBlocklistModal(true)}
+>
   <EyeSlashIcon className={'h-3'} />
 </Button>

Use this to verify Button forwards aria-label to a native element (expected: props spread onto <button>):

#!/bin/bash
set -euo pipefail

echo "Locate Common Button implementation:"
fd "index.tsx" src/components/Common/Button

echo
echo "Inspect Button props + render (look for ...props forwarding to <button>):"
fd "index.tsx" src/components/Common/Button --exec sed -n '1,260p'

echo
echo "Current icon-only standard buttons in TitleCard:"
rg -n -C2 "title-card-standard-icon-btn|aria-label" src/components/TitleCard/index.tsx

Also applies to: 398-400, 416-418

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

In `@src/components/TitleCard/index.tsx` around lines 385 - 387, The icon-only
standard Button instances in TitleCard (e.g., the Button with className
"title-card-standard-icon-btn" that calls onClickWatchlistBtn) lack accessible
names; add a descriptive aria-label (e.g., "Add to watchlist" or "Remove from
watchlist" as appropriate) to each icon-only Button usage in TitleCard (also
update the other occurrences around the referenced blocks). Also verify Common
Button (src/components/Common/Button index.tsx) forwards props such as
aria-label to the native <button> (ensure props are spread onto the rendered
<button>), so the aria-label reaches the DOM.
src/styles/globals.css (1)

658-662: ⚠️ Potential issue | 🟡 Minor

Fix the remaining Stylelint violation in this gradient block.

Line 662 is still failing declaration-empty-line-before, so this file will not lint clean until there's a blank line before background-image.

🧹 Minimal fix
 [data-theme='amoled-strix'] .collection-backdrop-overlay,
 [data-theme='amoled-strix'] .issue-backdrop-overlay {
   --tw-gradient-from: rgba(0, 0, 0, 0.6);
   --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);
+
   background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), rgb(17, 24, 39));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/styles/globals.css` around lines 658 - 662, The CSS block for
[data-theme='amoled-strix'] .collection-backdrop-overlay and
.issue-backdrop-overlay is missing a blank line before the background-image
declaration which triggers the stylelint rule declaration-empty-line-before; fix
it by adding a single blank line immediately above the background-image property
inside that rule so the declarations are separated (i.e., ensure there's an
empty line between the --tw-gradient-stops declaration and the background-image
declaration).
🧹 Nitpick comments (3)
src/components/Discover/HeroSlider/index.tsx (2)

214-229: Inline <style> tag is recreated on every render.

The @keyframes definitions inside the JSX are re-inserted into the DOM on each render, which is inefficient. Consider moving these animations to globals.css or a CSS module.

♻️ Move to globals.css

In src/styles/globals.css, add:

`@keyframes` heroSlideIn {
  from { opacity: 0; transform: translateX(22px); }
  to   { opacity: 1; transform: translateX(0); }
}
`@keyframes` heroSlideOut {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(-14px); }
}
.hero-item {
  animation-fill-mode: both;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
.hero-item-in  { animation-name: heroSlideIn; animation-duration: 500ms; }
.hero-item-out { animation-name: heroSlideOut; animation-duration: 250ms; }

Then remove the <style> tag from lines 214-229.

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

In `@src/components/Discover/HeroSlider/index.tsx` around lines 214 - 229, The
inline <style> block that defines the hero animations (keyframes heroSlideIn and
heroSlideOut and classes .hero-item, .hero-item-in, .hero-item-out) is being
recreated on every render; move these definitions into a persistent stylesheet
(e.g., add the `@keyframes` and the .hero-item / .hero-item-in / .hero-item-out
rules into src/styles/globals.css or a component CSS module) and then remove the
<style> tag from the HeroSlider JSX so the component references the class names
but no longer injects styles on each render.

78-90: Auto-rotation runs even when component is hidden.

The component is rendered inside <div className="discover-hero-slider hidden"> in src/components/Discover/index.tsx. By default (non-amoled-strix theme), the hidden class applies display: none, but the component still:

  1. Fetches trending data via SWR on mount
  2. Runs the 7-second rotation interval continuously
  3. Preloads 8 backdrop images

Consider pausing the interval when the component is not visible:

  1. Pass a visibility prop and conditionally start the interval, or
  2. Use IntersectionObserver to detect visibility, or
  3. Check document.hidden in the interval callback

This is a minor optimization for resource-conscious environments.

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

In `@src/components/Discover/HeroSlider/index.tsx` around lines 78 - 90, The
auto-rotation still runs when the slider is not visible; update the start/stop
logic so the interval managed by startInterval / intervalRef and the useEffect
that depends on items.length only starts when the component is visible: detect
visibility (choose one approach) and gate startInterval/clearInterval on it —
e.g., add a visible prop or create an IntersectionObserver in the component that
sets a local isVisible ref/state, then change the useEffect to call
startInterval only when isVisible && items.length > 1 and ensure the cleanup
clears intervalRef when !isVisible; also ensure the interval callback checks
document.hidden or isVisible (alongside isPaused.current) to avoid rotating
while hidden.
src/styles/globals.css (1)

670-679: Avoid CSS-only switching for duplicated media subtrees.

These selectors keep both theme variants mounted and just flip display. src/components/Layout/index.tsx:91-95 already does this for the two navbars, and the same pattern here now applies to headers, backdrops, and TitleCard variants. On media-heavy screens that adds extra React work and can cause unnecessary image/layout work for the hidden branch. Prefer gating duplicated subtrees in the component layer with the active theme and reserve CSS selectors for purely visual deltas.

Also applies to: 684-837

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

In `@src/styles/globals.css` around lines 670 - 679, The CSS is currently toggling
visibility by setting display for .layout-amoled-navbar /
.layout-standard-navbar and similar selectors which keeps both DOM subtrees
mounted and duplicates work; instead, gate those duplicated subtrees in the
component layer (same approach used in src/components/Layout/index.tsx for the
two navbars) so only the active variant is rendered. Update the Layout component
(or the components that render headers, backdrops, and TitleCard variants) to
switch on the active theme and render only the Amoled variant or the Standard
variant, then remove or simplify the corresponding CSS display toggles (e.g.,
rules targeting .layout-amoled-navbar, .layout-standard-navbar and the
duplicated header/backdrop/TitleCard selectors) so CSS only contains visual
deltas rather than mounting logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/TitleCard/index.tsx`:
- Around line 345-355: The AMOLED letter badge is decorative and should be
hidden from assistive tech; update the TitleCard badge element (the div with
class "title-card-amoled-badge" or the inner span that renders 'M/C/S') to
include an accessibility attribute such as aria-hidden="true" (or
role="presentation") so screen readers ignore it and ensure it remains
non-focusable.

In `@src/styles/globals.css`:
- Around line 7-8: The app flashes because ThemeContext sets data-theme in a
useEffect (ThemeContext -> useEffect) after hydration; modify
src/pages/_document.tsx to add a small blocking inline script in the <Html> that
runs before React mounts to read the persisted theme (localStorage key used by
ThemeContext) and set document.documentElement.setAttribute('data-theme', value)
(falling back to default on missing/invalid values), or alternatively thread the
preference through server-side props so the initial HTML already has data-theme
on the <Html> element; ensure the script uses the exact storage key your
ThemeContext expects and handles invalid values safely so the pre-seeded
attribute matches what ThemeContext will apply.

---

Duplicate comments:
In `@src/components/Discover/HeroSlider/index.tsx`:
- Around line 157-161: The onComplete handler unconditionally sets currentStatus
which can appear on a different slide if the carousel rotated after the request
started; update the onComplete handler in the component (the function handling
onComplete where setCurrentStatus, setShowRequestModal, setRequestTarget are
called) to first verify the requestTarget still matches the currently featured
item (e.g., compare requestTarget.id or unique key to the active slide id)
before calling setCurrentStatus, or alternatively implement pause-until-complete
behavior by setting isPaused.current = true when opening the modal and restoring
it on close so the slide cannot auto-rotate while a request is in progress.

In `@src/components/MovieDetails/index.tsx`:
- Around line 437-440: The JSX return in MovieDetails is broken because a
fragment <> is left open while a ternary's "else" branch `) : (` appears later
(invalid JSX); fix by importing and using the theme hook (e.g., useTheme) inside
the MovieDetails component to declare the condition (const { theme } =
useTheme(); const isAmoled = theme === 'amoled-strix'), then restructure the
return so the ternary is complete and each branch is wrapped in its own
top-level element (remove the stray fragment wrapper around the whole return and
ensure the conditionally rendered branches are returned as `isAmoled ?
(<AmoledLayout ... />) : (<DefaultLayout ... />)` or equivalent), and update any
references to the previous fragment children to live inside those branch
elements so the JSX compiles.

In `@src/components/TitleCard/index.tsx`:
- Around line 385-387: The icon-only standard Button instances in TitleCard
(e.g., the Button with className "title-card-standard-icon-btn" that calls
onClickWatchlistBtn) lack accessible names; add a descriptive aria-label (e.g.,
"Add to watchlist" or "Remove from watchlist" as appropriate) to each icon-only
Button usage in TitleCard (also update the other occurrences around the
referenced blocks). Also verify Common Button (src/components/Common/Button
index.tsx) forwards props such as aria-label to the native <button> (ensure
props are spread onto the rendered <button>), so the aria-label reaches the DOM.

In `@src/components/TvDetails/index.tsx`:
- Around line 468-471: The JSX return contains an orphaned ternary "):(" else
branch — fix by importing your theme hook (e.g., useTheme or reading
data-theme), create a boolean like const isAmoled = theme === 'amoled-strix',
and rewrite the top-level return to use isAmoled ? ( /* AMOLED layout JSX branch
*/ ) : ( /* standard layout JSX branch */ ), removing the stray fragment
wrappers so each branch is a valid JSX expression; ensure the fragment opened at
the original return is either removed or encompassed correctly inside the chosen
conditional and that any helper render functions (if present) are called inside
their respective branch.

In `@src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx`:
- Around line 47-55: The theme buttons in UserAppearanceSettings removed the
default focus outline and lack accessibility semantics; update the button
rendered in the map (the element with key={t.id} and onClick={() =>
setTheme(t.id)}) to include type="button", set aria-pressed={isActive} to
reflect selected state, and replace the removed focus outline with an explicit
visible focus-visible style (e.g. add focus-visible:ring and
focus-visible:ring-offset classes or similar accessible focus indicator
alongside any existing shadow/highlight classes) so keyboard users see focus.

In `@src/styles/globals.css`:
- Around line 658-662: The CSS block for [data-theme='amoled-strix']
.collection-backdrop-overlay and .issue-backdrop-overlay is missing a blank line
before the background-image declaration which triggers the stylelint rule
declaration-empty-line-before; fix it by adding a single blank line immediately
above the background-image property inside that rule so the declarations are
separated (i.e., ensure there's an empty line between the --tw-gradient-stops
declaration and the background-image declaration).

---

Nitpick comments:
In `@src/components/Discover/HeroSlider/index.tsx`:
- Around line 214-229: The inline <style> block that defines the hero animations
(keyframes heroSlideIn and heroSlideOut and classes .hero-item, .hero-item-in,
.hero-item-out) is being recreated on every render; move these definitions into
a persistent stylesheet (e.g., add the `@keyframes` and the .hero-item /
.hero-item-in / .hero-item-out rules into src/styles/globals.css or a component
CSS module) and then remove the <style> tag from the HeroSlider JSX so the
component references the class names but no longer injects styles on each
render.
- Around line 78-90: The auto-rotation still runs when the slider is not
visible; update the start/stop logic so the interval managed by startInterval /
intervalRef and the useEffect that depends on items.length only starts when the
component is visible: detect visibility (choose one approach) and gate
startInterval/clearInterval on it — e.g., add a visible prop or create an
IntersectionObserver in the component that sets a local isVisible ref/state,
then change the useEffect to call startInterval only when isVisible &&
items.length > 1 and ensure the cleanup clears intervalRef when !isVisible; also
ensure the interval callback checks document.hidden or isVisible (alongside
isPaused.current) to avoid rotating while hidden.

In `@src/styles/globals.css`:
- Around line 670-679: The CSS is currently toggling visibility by setting
display for .layout-amoled-navbar / .layout-standard-navbar and similar
selectors which keeps both DOM subtrees mounted and duplicates work; instead,
gate those duplicated subtrees in the component layer (same approach used in
src/components/Layout/index.tsx for the two navbars) so only the active variant
is rendered. Update the Layout component (or the components that render headers,
backdrops, and TitleCard variants) to switch on the active theme and render only
the Amoled variant or the Standard variant, then remove or simplify the
corresponding CSS display toggles (e.g., rules targeting .layout-amoled-navbar,
.layout-standard-navbar and the duplicated header/backdrop/TitleCard selectors)
so CSS only contains visual deltas rather than mounting logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 608a5375-fd03-46a1-b1ea-88559010b7bf

📥 Commits

Reviewing files that changed from the base of the PR and between c9db5b3 and 569cdee.

📒 Files selected for processing (9)
  • src/components/Discover/HeroSlider/index.tsx
  • src/components/Layout/AmoledNavbar/index.tsx
  • src/components/MovieDetails/index.tsx
  • src/components/Slider/index.tsx
  • src/components/TitleCard/index.tsx
  • src/components/TvDetails/index.tsx
  • src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx
  • src/pages/users/[userId]/settings/appearance.tsx
  • src/styles/globals.css
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/Slider/index.tsx
  • src/components/Layout/AmoledNavbar/index.tsx
  • src/pages/users/[userId]/settings/appearance.tsx

Comment thread src/components/TitleCard/index.tsx Outdated
Comment thread src/styles/globals.css
Introduces a second UI theme - AMOLED-Strix - alongside the existing
default dark theme. Designed for OLED/AMOLED displays with pure black
backgrounds and violet accents to maximize power savings.

## What's included

### Theme system
- New `ThemeContext` + `useTheme` hook with localStorage persistence
- Appearance settings page at `/profile/settings` and `/users/[userId]/settings`
- 7 semantic CSS color tokens (bg, surface, surface-raised, input, border,
  border-muted, ring) mapped to Tailwind utilities (bg-app, bg-surface, etc.)
- All gray-* and indigo-* Tailwind classes route through CSS variables so
  both themes update automatically

### AMOLED-Strix visual design
- Pure black (#000) background, near-black surfaces (#0e0e0e / #181818)
- Violet accent replacing indigo on TV content; indigo retained for movies
- Cinematic full-bleed hero on MovieDetails and TvDetails with backdrop blur,
  dark vignette, and staggered slide-in animations
- Redesigned TitleCard: glass badge, stronger gradient, glass action buttons
- Redesigned AmoledNavbar: floating glass search pill + profile dropdown
- HeroSlider on Discover page: crossfade backdrop, dot indicators, swipe support
- Redesigned MediaSlider section headers with accent divider bar
- AMOLED-specific flat content sections with dividers instead of glass cards

### Code quality
- Semantic token migration: removed all per-component `isAmoled` JS checks;
  structural differences handled via CSS `[data-theme='amoled-strix']` selectors
- Accessible names on all icon-only buttons (slider nav, title card actions,
  hero slider dots)
- i18n: all new UI strings go through intl.formatMessage (Crew, Keywords,
  Ratings, Details, See All, View Details, Go to slide N)
- Missing React keys added throughout
- Frozen request target in HeroSlider prevents wrong-item request on rotation
- currentIndex guard when SWR returns fewer trending items
- Composite mediaType+id key for HeroSlider backdrop stack
- Route guard (MANAGE_USERS) on /users/[userId]/settings/appearance
- Logout error handling in AmoledNavbar
- Defensive swatch defaults in UserAppearanceSettings
@RAFSuNX RAFSuNX force-pushed the feat/theming-system branch from cf03b1b to c5f5761 Compare March 14, 2026 15:18
RAFSuNX added 3 commits March 14, 2026 21:46
- Add aria-hidden to AMOLED badge div in TitleCard
- Add type=button and aria-pressed to theme picker buttons in UserAppearanceSettings
- Add missing deps (clear, setIsOpen) to AmoledNavbar useEffect
- Pause hero slider auto-rotation while request modal is open; resume on complete/cancel
- Add focus-visible ring to theme picker buttons in UserAppearanceSettings
- Add blank line before background-image in globals.css (stylelint)
- Move theme name/description out of ThemeContext into i18n messages
- Resolve localStorage synchronously before first render to eliminate theme flash
- Add pre-hydration inline script to _document.tsx as fallback flash prevention
Mount one ImageFader instead of two hidden via CSS display toggle.
AMOLED gradient overlay now controlled by a single CSS class
(person-fader-amoled-overlay) so only one interval and image set runs.
@RAFSuNX
Copy link
Copy Markdown
Author

RAFSuNX commented Mar 14, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

♻️ Duplicate comments (1)
src/components/TvDetails/index.tsx (1)

1364-1366: ⚠️ Potential issue | 🔴 Critical

Critical: Broken ternary syntax — missing conditional operator.

Line 1364 contains ) : ( but there is no matching ternary ? operator that opens this conditional branch. The code appears to intend switching between standard and AMOLED layouts, but the ternary expression is incomplete and will cause a syntax/parse error preventing compilation.

Additionally, line 1365's comment needs to be wrapped in JSX braces to comply with linting rules.

The fragment needs a proper ternary condition. Based on the PR's theme system, it should likely check the current theme:

🐛 Required fix
  return (
-   <>
-   <div
+   isAmoled ? (
+   /* ── AMOLED full-bleed layout ── */
+   <div className="relative bg-black">
+     {/* ... AMOLED layout ... */}
+   </div>
+   ) : (
+   <div
      className="tv-details-backdrop-section media-page"
      ...

Or wrap the entire return in a proper conditional:

return isAmoled ? (
  // AMOLED layout JSX
) : (
  // Standard layout JSX
);

Note: You'll need to import and use useTheme to get the isAmoled value:

import useTheme from '@app/hooks/useTheme';
// ...
const { theme } = useTheme();
const isAmoled = theme === 'amoled-strix';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TvDetails/index.tsx` around lines 1364 - 1366, The JSX
contains a broken ternary ") : (" without a matching "?" and an unwrapped JSX
comment; fix by importing and using useTheme (e.g., call useTheme() and derive
isAmoled via const isAmoled = theme === 'amoled-strix') and then wrap the return
in a proper ternary like return isAmoled ? (/* AMOLED JSX */) : (/* standard JSX
*/); also convert the inline comment on the AMOLED branch to a JSX comment using
{/* ... */} so linting passes; update references in this component
(TvDetails/index.tsx) to use the new isAmoled condition.
🧹 Nitpick comments (11)
src/components/Common/SlideOver/index.tsx (1)

74-74: Use semantic ring token for full theme consistency.

This container now uses bg-surface, but ring-gray-700 is still hard-coded. Switching to the semantic ring utility keeps SlideOver fully theme-driven across Default/AMOLED-Strix.

♻️ Suggested change
-<div className={`flex h-full flex-col rounded-lg shadow-xl ring-1 backdrop-blur bg-surface ring-gray-700`}>
+<div className="flex h-full flex-col rounded-lg bg-surface shadow-xl ring-1 ring-ring-app backdrop-blur">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Common/SlideOver/index.tsx` at line 74, The SlideOver
container div in the SlideOver component currently hard-codes the ring color
(ring-gray-700); replace that class with the semantic ring token used across the
design system (e.g., ring-outline) so the component follows theme tokens instead
of a fixed color — update the class string in the div inside the SlideOver
component (the line with `flex h-full flex-col rounded-lg shadow-xl ring-1
backdrop-blur bg-surface ring-gray-700`) to use the semantic ring class (replace
`ring-gray-700` with your project’s semantic ring utility).
src/components/Toast/index.tsx (1)

30-30: Use semantic ring token for full theme consistency.

On Line 30, bg-surface is theme-aware, but ring-gray-500 is still hardcoded. Switching to the semantic ring token keeps this toast aligned with the new token system across Default and AMOLED-Strix themes.

Proposed diff
-        <div className={`pointer-events-auto w-full max-w-sm rounded-lg shadow-lg ring-1 bg-surface ring-gray-500`}>
+        <div className="pointer-events-auto w-full max-w-sm rounded-lg bg-surface shadow-lg ring-1 ring-ring-app">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Toast/index.tsx` at line 30, Replace the hardcoded tailwind
class "ring-gray-500" with the semantic ring token so the Toast stays
theme-aware; update the class on the div in src/components/Toast/index.tsx (the
element with className starting "pointer-events-auto w-full max-w-sm...") to use
the semantic ring token (e.g., ring-ring) instead of ring-gray-500.
src/components/Common/Table/index.tsx (1)

9-9: Use plain string literals for static class strings.

Line 9 and Line 19 use template literals without interpolation. A plain string is simpler and avoids unnecessary syntax noise.

♻️ Suggested cleanup
-    <tbody className={`divide-y divide-border-default bg-surface`}>{children}</tbody>
+    <tbody className="divide-y divide-border-default bg-surface">{children}</tbody>
-    `px-4 py-3 bg-surface-raised text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider truncate`,
+    'px-4 py-3 bg-surface-raised text-left text-xs leading-4 font-medium text-gray-200 uppercase tracking-wider truncate',

Also applies to: 19-19

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

In `@src/components/Common/Table/index.tsx` at line 9, Replace the unnecessary
template literals used for static className values on the Table component's
tbody elements with plain string literals: change className={`divide-y
divide-border-default bg-surface`} (and the similar occurrence at line 19) to
use regular quoted strings so the JSX uses "divide-y divide-border-default
bg-surface" (and the corresponding static class string) instead of backtick
template syntax.
src/components/TitleCard/index.tsx (1)

488-490: Consider moving gradient to CSS custom property.

The inline gradient uses hardcoded rgba(45, 55, 72, ...) values. Per the PR's semantic token system, theme-aware colors should use CSS custom properties. Since this is wrapped in title-card-detail-link, the gradient could be defined in globals.css under [data-theme='amoled-strix'] for theme-specific overrides.

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

In `@src/components/TitleCard/index.tsx` around lines 488 - 490, The inline
background gradient in TitleCard (component TitleCard, JSX element with
class/title-card-detail-link) uses hardcoded rgba values; move that gradient
into a CSS custom property (e.g. --title-card-detail-gradient) defined in
globals.css and provide theme-specific overrides under
[data-theme='amoled-strix'] so the semantic token system drives colors, then
replace the inline style in TitleCard with a reference to that CSS variable.
Ensure the CSS custom property is set to the same gradient string and that the
JSX removes hardcoded rgba values and uses the property instead.
src/components/MovieDetails/index.tsx (1)

1145-1171: Consider hoisting shared modals outside the conditional branches.

The IssueModal, ManageSlideOver, and BlocklistModal components are identically duplicated in both the standard layout (lines 459-485) and AMOLED layout (lines 1145-1171). Since these modals are controlled by the same state variables and have identical props, they could be rendered once outside the conditional to reduce duplication and maintenance burden.

♻️ Suggested structure
  return (
    <>
+     {/* Shared modals - rendered once regardless of theme */}
+     <PageTitle title={data.title} />
+     <IssueModal ... />
+     <ManageSlideOver ... />
+     <BlocklistModal ... />
+
      {isAmoledTheme ? (
        <div className="relative bg-black">
-         <PageTitle title={data.title} />
-         <IssueModal ... />
-         <ManageSlideOver ... />
-         <BlocklistModal ... />
          {/* Hero and content sections */}
          ...
        </div>
      ) : (
        <div className="movie-details-backdrop-section media-page" ...>
-         <PageTitle title={data.title} />
-         <IssueModal ... />
-         <ManageSlideOver ... />
-         <BlocklistModal ... />
          {/* Standard layout content */}
          ...
        </div>
      )}
    </>
  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MovieDetails/index.tsx` around lines 1145 - 1171, Duplicate
modal components (IssueModal, ManageSlideOver, BlocklistModal) are rendered in
both layout branches; hoist them out of the conditional so they render once and
use the same state/handlers. Remove the duplicated instances inside the standard
and AMOLED branches and place a single set of IssueModal (props:
show={showIssueModal}, onCancel={() => setShowIssueModal(false)},
mediaType="movie", tmdbId={data.id}), ManageSlideOver (props: data={data},
mediaType="movie", show={showManager}, onClose={() => { setShowManager(false);
router.push({ pathname: router.pathname, query: { movieId: router.query.movieId
} }); }}, revalidate={() => revalidate()}), and BlocklistModal (props:
tmdbId={data.id}, type="movie", show={showBlocklistModal},
onCancel={closeBlocklistModal}, onComplete={onClickHideItemBtn},
isUpdating={isBlocklistUpdating}) immediately outside the conditional that
chooses standard vs AMOLED layout so all branches share these components and
their state.
src/components/UserProfile/index.tsx (1)

156-156: Minor inconsistency: ring-gray-700 mixed with semantic bg-surface.

The first quota card uses ring-gray-700 (hardcoded) while the other two conditionally use ring-gray-700 as well. For full theme consistency, consider using ring-ring-app/20 or ring-border-default to ensure proper appearance in both default and AMOLED themes.

♻️ Suggested fix for consistency
-              <div className={`overflow-hidden px-4 py-5 sm:p-6 rounded-lg bg-surface shadow ring-1 ring-gray-700`}>
+              <div className={`overflow-hidden px-4 py-5 sm:p-6 rounded-lg bg-surface shadow ring-1 ring-border-default`}>

Similarly for lines 176 and 227, the non-restricted fallback case:

-${quota.movie.restricted ? 'bg-gradient-to-t from-red-900 to-transparent ring-red-500' : 'ring-gray-700'}
+${quota.movie.restricted ? 'bg-gradient-to-t from-red-900 to-transparent ring-red-500' : 'ring-border-default'}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/UserProfile/index.tsx` at line 156, Replace the hardcoded ring
class used on the quota card div in the UserProfile component with the semantic
ring token used elsewhere so theme appearance stays consistent; specifically,
update the div that currently uses `ring-gray-700` (and the equivalent
non-restricted fallback cases referenced near the other quota card blocks) to
use the semantic token such as `ring-ring-app/20` or `ring-border-default` so
all quota card containers (the div with classes including `overflow-hidden px-4
py-5 sm:p-6 rounded-lg bg-surface shadow ring-1`) use the same theme-aware ring
class.
src/context/ThemeContext.tsx (1)

55-59: Consider guarding setTheme against SSR edge cases.

While setTheme is typically only called from client-side interactions, adding a guard would make the code more defensive against potential SSR edge cases (e.g., if a component accidentally calls it during server rendering).

♻️ Optional defensive guard
   const setTheme = useCallback((next: ThemeId) => {
     setThemeState(next);
+    if (typeof window === 'undefined') return;
     localStorage.setItem(STORAGE_KEY, next);
     document.documentElement.setAttribute('data-theme', next);
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/context/ThemeContext.tsx` around lines 55 - 59, The setTheme callback
should be defensive for SSR: keep calling setThemeState(next) but wrap the
client-only operations (localStorage.setItem and
document.documentElement.setAttribute) in a runtime guard (e.g., check typeof
window !== 'undefined' or typeof document !== 'undefined' and typeof
localStorage !== 'undefined') so setTheme (the useCallback that calls
setThemeState, localStorage.setItem, and document.documentElement.setAttribute)
won't throw during server rendering; update the setTheme body to perform the
guard before accessing localStorage or document.
src/components/Setup/JellyfinSetup.tsx (1)

175-177: Theme token migration looks good.

The hostname prefix span now uses semantic tokens (border-border-default, bg-input) instead of hardcoded border-gray-500/bg-gray-800, enabling proper theme adaptation.

Minor: The template literal is unnecessary since there are no interpolated values.

♻️ Simplify to plain string
-<span className={`inline-flex cursor-default items-center rounded-l-md border border-r-0 px-3 text-gray-100 sm:text-sm border-border-default bg-input`}>
+<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 px-3 text-gray-100 sm:text-sm border-border-default bg-input">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Setup/JellyfinSetup.tsx` around lines 175 - 177, The span's
className currently uses a template literal without interpolation; change the
className in the JSX span (the hostname prefix span that renders {values.useSsl
? 'https://' : 'http://'}) from a template literal to a plain string literal
containing the classes (keep border-border-default, bg-input, inline-flex,
cursor-default, items-center, rounded-l-md, border, border-r-0, px-3,
text-gray-100, sm:text-sm) to simplify the code.
src/components/Discover/HeroSlider/index.tsx (1)

216-231: Inline <style> tag creates duplicate styles on every render.

The <style> element inside the component body will be inserted into the DOM on every render, potentially creating duplicate style definitions. Consider moving these keyframe animations to globals.css or using a CSS-in-JS solution that handles deduplication.

♻️ Move animations to globals.css

Add to src/styles/globals.css:

`@keyframes` heroSlideIn {
  from { opacity: 0; transform: translateX(22px); }
  to   { opacity: 1; transform: translateX(0); }
}
`@keyframes` heroSlideOut {
  from { opacity: 1; transform: translateX(0); }
  to   { opacity: 0; transform: translateX(-14px); }
}
.hero-item {
  animation-fill-mode: both;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
.hero-item-in  { animation-name: heroSlideIn; animation-duration: 500ms; }
.hero-item-out { animation-name: heroSlideOut; animation-duration: 250ms; }

Then remove lines 216-231 from the component.

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

In `@src/components/Discover/HeroSlider/index.tsx` around lines 216 - 231, The
inline <style> block in the HeroSlider component is creating duplicate keyframe
and class rules on each render; move the keyframes (heroSlideIn, heroSlideOut)
and the .hero-item / .hero-item-in / .hero-item-out rules into your global
stylesheet (e.g., src/styles/globals.css) and then remove the
<style>{`...`}</style> block from the HeroSlider component (index.tsx) so the
component simply relies on the global CSS classes; keep the class names
unchanged so existing JSX that uses "hero-item", "hero-item-in", and
"hero-item-out" continues to work.
src/components/Layout/AmoledNavbar/index.tsx (1)

100-108: Inline <style> tag should be moved to globals.css.

Similar to HeroSlider, this inline style block will be duplicated on each render. Move the iconPop keyframe animation to globals.css.

♻️ Move to globals.css
`@keyframes` iconPop {
  from { opacity: 0; transform: scale(0.6) rotate(-15deg); }
  to   { opacity: 1; transform: scale(1) rotate(0deg); }
}
.icon-pop {
  animation: iconPop 250ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Layout/AmoledNavbar/index.tsx` around lines 100 - 108, Remove
the inline <style> block in the AmoledNavbar component (index.tsx) and add the
keyframes and .icon-pop rule to globals.css instead; specifically move the
iconPop keyframe and .icon-pop animation declaration into globals.css, keep
using the same className ("icon-pop") in the component markup, and delete the
style template literal in the AmoledNavbar file (and remove any duplicated
inline style in HeroSlider if present) so the animation is defined globally and
not re-inserted on each render.
src/components/RequestList/index.tsx (1)

276-276: Use semantic hover tokens to avoid theme drift.

Line 276 still uses hard-coded hover:!bg-gray-400, which can look off in AMOLED mode. Consider a semantic surface token for hover so this control stays theme-consistent.

♻️ Proposed tweak
- className="z-40 mr-2 rounded-l-none border border-border-default !bg-input !px-3 !text-gray-500 hover:!bg-gray-400 hover:!text-white"
+ className="z-40 mr-2 rounded-l-none border border-border-default !bg-input !px-3 !text-gray-500 hover:!bg-surface-raised hover:!text-white"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/RequestList/index.tsx` at line 276, The button/input class
string in RequestList (the className that currently contains
"hover:!bg-gray-400") uses a hard-coded color token which causes theme drift;
replace the hard-coded hover utility with your design-system semantic hover
token (e.g., "hover:!bg-surface-hover" or the project's equivalent token name)
in the same className string so the control uses the theme surface hover style
and remains consistent across light/dark/AMOLED modes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Common/ButtonWithDropdown/index.tsx`:
- Around line 35-40: The 'glass' variant branch in ButtonWithDropdown removes
the default focus outline but doesn't add a visible focus-visible indicator;
update the class lists built in the case 'glass' block for
styleClasses.mainButtonClasses and styleClasses.dropdownSideButtonClasses (and
the other two similar glass assignments at the other occurrences) to append
accessible focus-visible utility classes (e.g., focus-visible:ring-2
focus-visible:ring-white/40 focus-visible:ring-offset-2 or equivalent) so
keyboard focus is clearly visible while preserving the visual glass styling.

In `@src/components/Common/Dropdown/index.tsx`:
- Around line 28-30: The glass variant suppresses the default outline and needs
an explicit visible keyboard focus state: update the Dropdown component logic
that checks buttonType === 'glass' to append a focus-visible utility (e.g.,
focus-visible:outline, focus-visible:ring-2, focus-visible:ring-white/[.xx], or
focus-visible:ring-offset) to the class string used for the trigger and for
dropdown items (the code paths handling the trigger control and item controls
referenced around the same conditional), so keyboard users get a clear visible
ring/outline when focused while preserving the glass look.

In `@src/components/Discover/DiscoverMovies/index.tsx`:
- Around line 67-68: The hidden AMOLED backdrop logic must be gated by the
current theme to avoid fetching large images for users not using AMOLED; update
the effects and handlers that set backdropPath (and related logic that uses
pickedRef) to first check the theme preference (e.g., an isAmoled or theme value
from your theme context/prop) and only call setBackdropPath or initiate the
backdrop download when the theme indicates AMOLED/dark mode is active and the
user opted-in (pickedRef). Concretely, in the functions/effects that reference
backdropPath, setBackdropPath and pickedRef (and the same blocks currently
around lines where backdrop is set/cleared) add a guard like if (!isAmoled)
return so the hidden backdrop is never resolved/fetched for non-AMOLED themes.

In `@src/components/Discover/DiscoverTv/index.tsx`:
- Around line 53-55: The backdrop <img> elements in the DiscoverTv component are
still instantiated even when the AMOLED theme hides them, causing unnecessary
image loads; update the JSX to only create/render those <img> elements when the
active theme allows backdrops (e.g., check a theme flag from context or props
such as isAmoledTheme/useTheme) and when backdropPath is non-null—apply this
conditional gating where backdropPath/setBackdropPath is used and at the other
image spots referenced (around the blocks you noted at 68-75 and 86-102); ensure
you do not merely hide via CSS but short-circuit rendering (e.g., render
{allowBackdrop && backdropPath && <img ... />}), and avoid setting src until the
condition passes.

In `@src/components/Layout/AmoledNavbar/index.tsx`:
- Around line 70-77: The logout function currently calls revalidate() but
doesn't invalidate Layout's SWR caches for request/issue counts, so stale counts
can persist; update the logout handler (function logout) to import and call
SWR's global mutate to clear those keys (e.g., call
mutate('/api/v1/request/count') and mutate('/api/v1/issue/count') or a single
mutate() to clear all caches) immediately after a successful logout response and
before any redirect so Layout will reflect the logged-out state.

In `@src/components/MovieDetails/index.tsx`:
- Around line 1140-1143: The JSX contains a broken ternary and an unbraced
comment in the MovieDetails component; fix it by importing and calling the
existing useTheme hook (from src/hooks/useTheme.ts) inside the MovieDetails
component to obtain theme, use a conditional like theme === 'amoled-strix' as
the ternary condition to choose between the standard layout and the AMOLED
full-bleed layout (replace the stray ") : (" with ") ? (standardLayout) :
(amoledLayout)"), and ensure any inline JSX comment is wrapped in braces (e.g.,
{/* ... */}) so the JSX compiles correctly; identify the standard layout block
and the AMOLED block by the surrounding divs and className "relative bg-black"
to place them as the true/false branches.

In `@src/components/TitleCard/index.tsx`:
- Around line 385-387: The icon-only <Button> components lack accessible names;
update the Button instances for the watchlist, remove-watchlist and blocklist
controls (the components using props/onClick handlers onClickWatchlistBtn,
onClickRemoveWatchlistBtn, onClickBlocklistBtn and the one rendering <StarIcon>
etc.) to include an appropriate aria-label prop (e.g., aria-label="Add to
watchlist", "Remove from watchlist", "Block user") matching the native button
labels so screen readers get an accessible name.

---

Duplicate comments:
In `@src/components/TvDetails/index.tsx`:
- Around line 1364-1366: The JSX contains a broken ternary ") : (" without a
matching "?" and an unwrapped JSX comment; fix by importing and using useTheme
(e.g., call useTheme() and derive isAmoled via const isAmoled = theme ===
'amoled-strix') and then wrap the return in a proper ternary like return
isAmoled ? (/* AMOLED JSX */) : (/* standard JSX */); also convert the inline
comment on the AMOLED branch to a JSX comment using {/* ... */} so linting
passes; update references in this component (TvDetails/index.tsx) to use the new
isAmoled condition.

---

Nitpick comments:
In `@src/components/Common/SlideOver/index.tsx`:
- Line 74: The SlideOver container div in the SlideOver component currently
hard-codes the ring color (ring-gray-700); replace that class with the semantic
ring token used across the design system (e.g., ring-outline) so the component
follows theme tokens instead of a fixed color — update the class string in the
div inside the SlideOver component (the line with `flex h-full flex-col
rounded-lg shadow-xl ring-1 backdrop-blur bg-surface ring-gray-700`) to use the
semantic ring class (replace `ring-gray-700` with your project’s semantic ring
utility).

In `@src/components/Common/Table/index.tsx`:
- Line 9: Replace the unnecessary template literals used for static className
values on the Table component's tbody elements with plain string literals:
change className={`divide-y divide-border-default bg-surface`} (and the similar
occurrence at line 19) to use regular quoted strings so the JSX uses "divide-y
divide-border-default bg-surface" (and the corresponding static class string)
instead of backtick template syntax.

In `@src/components/Discover/HeroSlider/index.tsx`:
- Around line 216-231: The inline <style> block in the HeroSlider component is
creating duplicate keyframe and class rules on each render; move the keyframes
(heroSlideIn, heroSlideOut) and the .hero-item / .hero-item-in / .hero-item-out
rules into your global stylesheet (e.g., src/styles/globals.css) and then remove
the <style>{`...`}</style> block from the HeroSlider component (index.tsx) so
the component simply relies on the global CSS classes; keep the class names
unchanged so existing JSX that uses "hero-item", "hero-item-in", and
"hero-item-out" continues to work.

In `@src/components/Layout/AmoledNavbar/index.tsx`:
- Around line 100-108: Remove the inline <style> block in the AmoledNavbar
component (index.tsx) and add the keyframes and .icon-pop rule to globals.css
instead; specifically move the iconPop keyframe and .icon-pop animation
declaration into globals.css, keep using the same className ("icon-pop") in the
component markup, and delete the style template literal in the AmoledNavbar file
(and remove any duplicated inline style in HeroSlider if present) so the
animation is defined globally and not re-inserted on each render.

In `@src/components/MovieDetails/index.tsx`:
- Around line 1145-1171: Duplicate modal components (IssueModal,
ManageSlideOver, BlocklistModal) are rendered in both layout branches; hoist
them out of the conditional so they render once and use the same state/handlers.
Remove the duplicated instances inside the standard and AMOLED branches and
place a single set of IssueModal (props: show={showIssueModal}, onCancel={() =>
setShowIssueModal(false)}, mediaType="movie", tmdbId={data.id}), ManageSlideOver
(props: data={data}, mediaType="movie", show={showManager}, onClose={() => {
setShowManager(false); router.push({ pathname: router.pathname, query: {
movieId: router.query.movieId } }); }}, revalidate={() => revalidate()}), and
BlocklistModal (props: tmdbId={data.id}, type="movie",
show={showBlocklistModal}, onCancel={closeBlocklistModal},
onComplete={onClickHideItemBtn}, isUpdating={isBlocklistUpdating}) immediately
outside the conditional that chooses standard vs AMOLED layout so all branches
share these components and their state.

In `@src/components/RequestList/index.tsx`:
- Line 276: The button/input class string in RequestList (the className that
currently contains "hover:!bg-gray-400") uses a hard-coded color token which
causes theme drift; replace the hard-coded hover utility with your design-system
semantic hover token (e.g., "hover:!bg-surface-hover" or the project's
equivalent token name) in the same className string so the control uses the
theme surface hover style and remains consistent across light/dark/AMOLED modes.

In `@src/components/Setup/JellyfinSetup.tsx`:
- Around line 175-177: The span's className currently uses a template literal
without interpolation; change the className in the JSX span (the hostname prefix
span that renders {values.useSsl ? 'https://' : 'http://'}) from a template
literal to a plain string literal containing the classes (keep
border-border-default, bg-input, inline-flex, cursor-default, items-center,
rounded-l-md, border, border-r-0, px-3, text-gray-100, sm:text-sm) to simplify
the code.

In `@src/components/TitleCard/index.tsx`:
- Around line 488-490: The inline background gradient in TitleCard (component
TitleCard, JSX element with class/title-card-detail-link) uses hardcoded rgba
values; move that gradient into a CSS custom property (e.g.
--title-card-detail-gradient) defined in globals.css and provide theme-specific
overrides under [data-theme='amoled-strix'] so the semantic token system drives
colors, then replace the inline style in TitleCard with a reference to that CSS
variable. Ensure the CSS custom property is set to the same gradient string and
that the JSX removes hardcoded rgba values and uses the property instead.

In `@src/components/Toast/index.tsx`:
- Line 30: Replace the hardcoded tailwind class "ring-gray-500" with the
semantic ring token so the Toast stays theme-aware; update the class on the div
in src/components/Toast/index.tsx (the element with className starting
"pointer-events-auto w-full max-w-sm...") to use the semantic ring token (e.g.,
ring-ring) instead of ring-gray-500.

In `@src/components/UserProfile/index.tsx`:
- Line 156: Replace the hardcoded ring class used on the quota card div in the
UserProfile component with the semantic ring token used elsewhere so theme
appearance stays consistent; specifically, update the div that currently uses
`ring-gray-700` (and the equivalent non-restricted fallback cases referenced
near the other quota card blocks) to use the semantic token such as
`ring-ring-app/20` or `ring-border-default` so all quota card containers (the
div with classes including `overflow-hidden px-4 py-5 sm:p-6 rounded-lg
bg-surface shadow ring-1`) use the same theme-aware ring class.

In `@src/context/ThemeContext.tsx`:
- Around line 55-59: The setTheme callback should be defensive for SSR: keep
calling setThemeState(next) but wrap the client-only operations
(localStorage.setItem and document.documentElement.setAttribute) in a runtime
guard (e.g., check typeof window !== 'undefined' or typeof document !==
'undefined' and typeof localStorage !== 'undefined') so setTheme (the
useCallback that calls setThemeState, localStorage.setItem, and
document.documentElement.setAttribute) won't throw during server rendering;
update the setTheme body to perform the guard before accessing localStorage or
document.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19535b0b-8418-47f3-ace4-e9ef2ea457d9

📥 Commits

Reviewing files that changed from the base of the PR and between 569cdee and fb0c437.

📒 Files selected for processing (71)
  • src/components/Blocklist/index.tsx
  • src/components/CollectionDetails/index.tsx
  • src/components/Common/ButtonWithDropdown/index.tsx
  • src/components/Common/Dropdown/index.tsx
  • src/components/Common/ImageFader/index.tsx
  • src/components/Common/Modal/index.tsx
  • src/components/Common/PlayButton/index.tsx
  • src/components/Common/SettingsTabs/index.tsx
  • src/components/Common/SlideOver/index.tsx
  • src/components/Common/Table/index.tsx
  • src/components/Common/Tag/index.tsx
  • src/components/Common/Tooltip/index.tsx
  • src/components/Discover/DiscoverMovies/index.tsx
  • src/components/Discover/DiscoverSliderEdit/index.tsx
  • src/components/Discover/DiscoverTv/index.tsx
  • src/components/Discover/HeroSlider/index.tsx
  • src/components/Discover/Trending.tsx
  • src/components/Discover/index.tsx
  • src/components/IssueDetails/IssueComment/index.tsx
  • src/components/IssueDetails/index.tsx
  • src/components/IssueList/IssueItem/index.tsx
  • src/components/IssueList/index.tsx
  • src/components/IssueModal/CreateIssueModal/index.tsx
  • src/components/Layout/AmoledNavbar/index.tsx
  • src/components/Layout/PullToRefresh/index.tsx
  • src/components/Layout/UserDropdown/index.tsx
  • src/components/Layout/index.tsx
  • src/components/Login/JellyfinLogin.tsx
  • src/components/Login/LocalLogin.tsx
  • src/components/Login/index.tsx
  • src/components/MediaSlider/index.tsx
  • src/components/MovieDetails/index.tsx
  • src/components/PersonDetails/index.tsx
  • src/components/RegionSelector/index.tsx
  • src/components/RequestBlock/index.tsx
  • src/components/RequestButton/index.tsx
  • src/components/RequestCard/index.tsx
  • src/components/RequestList/RequestItem/index.tsx
  • src/components/RequestList/index.tsx
  • src/components/RequestModal/AdvancedRequester/index.tsx
  • src/components/ResetPassword/RequestResetLink.tsx
  • src/components/ResetPassword/index.tsx
  • src/components/Settings/OverrideRule/OverrideRuleTiles.tsx
  • src/components/Settings/SettingsAbout/Releases/index.tsx
  • src/components/Settings/SettingsJellyfin.tsx
  • src/components/Settings/SettingsLogs/index.tsx
  • src/components/Settings/SettingsMetadata.tsx
  • src/components/Settings/SettingsPlex.tsx
  • src/components/Settings/SettingsServices.tsx
  • src/components/Setup/JellyfinSetup.tsx
  • src/components/Setup/index.tsx
  • src/components/Slider/index.tsx
  • src/components/TitleCard/ErrorCard.tsx
  • src/components/TitleCard/index.tsx
  • src/components/Toast/index.tsx
  • src/components/TvDetails/Season/index.tsx
  • src/components/TvDetails/index.tsx
  • src/components/UserList/index.tsx
  • src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx
  • src/components/UserProfile/UserSettings/UserLinkedAccountsSettings/index.tsx
  • src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem.tsx
  • src/components/UserProfile/UserSettings/index.tsx
  • src/components/UserProfile/index.tsx
  • src/context/ThemeContext.tsx
  • src/hooks/useTheme.ts
  • src/pages/_app.tsx
  • src/pages/_document.tsx
  • src/pages/profile/settings/appearance.tsx
  • src/pages/users/[userId]/settings/appearance.tsx
  • src/styles/globals.css
  • tailwind.config.js
🚧 Files skipped from review as they are similar to previous changes (39)
  • src/components/Common/PlayButton/index.tsx
  • src/components/PersonDetails/index.tsx
  • src/components/Discover/DiscoverSliderEdit/index.tsx
  • src/components/IssueModal/CreateIssueModal/index.tsx
  • src/components/Slider/index.tsx
  • src/components/Settings/SettingsMetadata.tsx
  • src/components/UserProfile/UserSettings/UserAppearanceSettings/index.tsx
  • src/components/RequestBlock/index.tsx
  • src/components/Settings/SettingsAbout/Releases/index.tsx
  • src/pages/users/[userId]/settings/appearance.tsx
  • src/components/Layout/index.tsx
  • src/pages/profile/settings/appearance.tsx
  • src/components/Discover/Trending.tsx
  • src/components/Setup/index.tsx
  • src/components/TvDetails/Season/index.tsx
  • src/components/Settings/SettingsJellyfin.tsx
  • src/components/Settings/SettingsLogs/index.tsx
  • src/components/Common/ImageFader/index.tsx
  • src/components/Settings/OverrideRule/OverrideRuleTiles.tsx
  • src/components/Layout/PullToRefresh/index.tsx
  • src/components/Common/SettingsTabs/index.tsx
  • src/components/IssueDetails/index.tsx
  • src/components/RegionSelector/index.tsx
  • src/components/UserList/index.tsx
  • src/components/Layout/UserDropdown/index.tsx
  • tailwind.config.js
  • src/components/IssueDetails/IssueComment/index.tsx
  • src/components/IssueList/IssueItem/index.tsx
  • src/components/MediaSlider/index.tsx
  • src/components/Common/Modal/index.tsx
  • src/components/Common/Tooltip/index.tsx
  • src/components/Login/JellyfinLogin.tsx
  • src/components/Discover/index.tsx
  • src/components/Settings/SettingsPlex.tsx
  • src/components/Blocklist/index.tsx
  • src/components/IssueList/index.tsx
  • src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsWebPush/DeviceItem.tsx
  • src/components/Common/Tag/index.tsx
  • src/components/Settings/SettingsServices.tsx

Comment thread src/components/Common/ButtonWithDropdown/index.tsx
Comment thread src/components/Common/Dropdown/index.tsx
Comment thread src/components/Discover/DiscoverMovies/index.tsx
Comment thread src/components/Discover/DiscoverTv/index.tsx
Comment thread src/components/Layout/AmoledNavbar/index.tsx
Comment thread src/components/MovieDetails/index.tsx
Comment thread src/components/TitleCard/index.tsx Outdated
- Fix critical broken ternary in MovieDetails and TvDetails (missing
  isAmoledTheme condition, unbraced JSX comments)
- Add focus-visible ring to glass variant in ButtonWithDropdown and Dropdown
- Gate AMOLED backdrop image in DiscoverMovies and DiscoverTv behind
  isAmoledTheme to prevent unnecessary image loads
- Add aria-label to icon-only Button components in TitleCard
@RAFSuNX
Copy link
Copy Markdown
Author

RAFSuNX commented Mar 14, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
src/components/TvDetails/index.tsx (1)

1369-1371: ⚠️ Potential issue | 🔴 Critical

Fix invalid JSX in the ternary else branch.

At Line 1369, the : ( branch starts, then Line 1370 adds a standalone JSX comment before the <div> on Line 1371. That produces an invalid branch expression and matches the parse failures.

🔧 Proposed fix
-    ) : (
-    {/* ── AMOLED full-bleed layout ── */}
-    <div className="relative bg-black">
+    ) : (
+    <div className="relative bg-black">
+      {/* ── AMOLED full-bleed layout ── */}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TvDetails/index.tsx` around lines 1369 - 1371, The JSX ternary
else branch in the TvDetails component contains a standalone JSX comment between
the opening parenthesis and the root element, causing a parse error; remove or
move the JSX comment so the else branch directly returns the <div
className="relative bg-black"> element (or wrap the comment inside the returned
fragment/element) in the ternary expression used in the TvDetails component to
restore valid JSX.
src/components/MovieDetails/index.tsx (1)

1146-1148: ⚠️ Potential issue | 🔴 Critical

The else branch still contains invalid JSX structure.

At Line 1146 the ternary else branch starts, Line 1147 inserts a standalone JSX comment, then Line 1148 starts the <div>. This creates the same parse break pattern as before.

🔧 Proposed fix
-      ) : (
-      {/* ── AMOLED full-bleed layout ── */}
-      <div className="relative bg-black">
+      ) : (
+      <div className="relative bg-black">
+        {/* ── AMOLED full-bleed layout ── */}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MovieDetails/index.tsx` around lines 1146 - 1148, The else
branch of the ternary in MovieDetails (the branch beginning at the ternary else
around the AMOLED full-bleed layout) contains a standalone JSX comment that
breaks parsing; move the JSX comment so it is inside a JSX element (e.g., place
the comment as the first child inside the <div className="relative bg-black">)
or convert it to a JS/TS comment above the return/JSX expression, ensuring the
else branch starts immediately with a valid JSX node (the <div> or a fragment)
rather than a standalone comment.
🧹 Nitpick comments (4)
src/components/Discover/DiscoverMovies/index.tsx (1)

111-114: Potential duplicate heading accessibility concern.

Both the h1 (line 111) and Header component (likely also an h1) are rendered to the DOM simultaneously, with visibility toggled via CSS. If the CSS uses display: none to hide the inactive heading, this is acceptable. However, if it uses visibility: hidden or opacity: 0, screen readers may announce both headings.

Consider verifying the CSS ensures display: none for the hidden variant, or conditionally render only one heading based on isAmoledTheme to eliminate the duplication entirely:

♻️ Optional refactor to conditionally render single heading
       <div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-end">
-        <h1 className="discover-movies-amoled-heading mt-5 text-4xl font-bold tracking-tight text-blue-300">{title}</h1>
-        <div className="discover-movies-standard-heading">
-          <Header>{title}</Header>
-        </div>
+        {isAmoledTheme ? (
+          <h1 className="mt-5 text-4xl font-bold tracking-tight text-blue-300">{title}</h1>
+        ) : (
+          <Header>{title}</Header>
+        )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Discover/DiscoverMovies/index.tsx` around lines 111 - 114,
There are two headings rendered: the literal h1 element and the Header component
(both using {title}), which can cause duplicate announcements for screen
readers; update the DiscoverMovies component to render only one heading at a
time by conditionally rendering the h1 or the Header based on the theme flag
(e.g., isAmoledTheme) or ensure the hidden variant uses display: none in CSS;
locate the JSX that contains the <h1 className="discover-movies-amoled-heading
...">{title}</h1> and the <Header>{title}</Header> and either wrap them in a
conditional (render h1 when isAmoledTheme true else render Header) or change the
CSS for discover-movies-amoled-heading / discover-movies-standard-heading to use
display: none for the inactive state so screen readers won’t see both.
src/components/TitleCard/index.tsx (3)

377-419: Add type="button" to native button elements.

The native <button> elements (lines 378, 391, 409) lack the type="button" attribute. While not strictly necessary outside forms, it's a defensive best practice to prevent unintended form submission if this component is ever nested inside a form.

🛠️ Suggested fix
 <button
+  type="button"
   aria-label={intl.formatMessage(messages.addToWatchList)}
   className="title-card-amoled-icon-btn ..."
   onClick={onClickWatchlistBtn}
 >

Apply the same pattern to all native <button> elements (lines 391, 409).

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

In `@src/components/TitleCard/index.tsx` around lines 377 - 419, Native <button>
elements in the TitleCard component are missing type="button", which can cause
unexpected form submissions if nested in a form; update each native button
(those using onClickWatchlistBtn, onClickDeleteWatchlistBtn, and the one that
calls setShowBlocklistModal/EyeSlashIcon) to include type="button" so they
behave as non-submitting buttons while leaving the existing Button component
instances unchanged.

579-602: Add type="button" to the AMOLED request button.

Consistent with the other native buttons, add type="button" to prevent potential form submission issues.

🛠️ Suggested fix
 <button
+  type="button"
   onClick={(e) => {
     e.preventDefault();
     setShowRequestModal(true);
   }}
   className="title-card-amoled-request-btn ..."
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TitleCard/index.tsx` around lines 579 - 602, The AMOLED
request native button (the <button> with className
"title-card-amoled-request-btn" that calls setShowRequestModal in its onClick)
is missing an explicit type, which can cause unintended form submissions; add
type="button" to that button element so it behaves like the other native button
and only triggers setShowRequestModal.

536-568: Consider using Tailwind's line-clamp utility instead of inline styles.

The inline WebkitLineClamp styles could be replaced with Tailwind's line-clamp-* utilities for consistency and reduced inline style overhead.

♻️ Example refactor for title
 <h1
-  className="whitespace-normal text-xl font-bold leading-tight"
-  style={{
-    WebkitLineClamp: 3,
-    display: '-webkit-box',
-    overflow: 'hidden',
-    WebkitBoxOrient: 'vertical',
-    wordBreak: 'break-word',
-  }}
+  className="whitespace-normal text-xl font-bold leading-tight line-clamp-3 break-words"
   data-testid="title-card-title"
 >

Apply similarly to the summary div with dynamic line clamp values (may require conditional class names).

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

In `@src/components/TitleCard/index.tsx` around lines 536 - 568, Replace the
inline WebkitLineClamp/display/WebkitBoxOrient/overflow styles in the TitleCard
component: for the title h1 (data-testid="title-card-title") swap those inline
styles for Tailwind's line-clamp utility (e.g., className includes
"line-clamp-3"); for the summary div replace the computed inline WebkitLineClamp
with conditional Tailwind classes (e.g., choose between "line-clamp-3" or
"line-clamp-5" based on showRequestButton and currentStatus/MediaStatus checks)
by building the className string accordingly; remove the redundant inline style
properties (display, overflow, WebkitBoxOrient, wordBreak if already covered)
and ensure the conditional logic that references showRequestButton,
currentStatus and MediaStatus stays intact when computing the className for the
summary.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/MovieDetails/index.tsx`:
- Around line 1299-1305: The icon-only buttons that open modals (e.g., the
button invoking setShowBlocklistModal and rendering EyeSlashIcon and other
similar icon buttons) lack accessible names and explicit button types; update
each icon-only button to include a descriptive aria-label (e.g.,
aria-label="Open blocklist modal" or appropriate action text) and add
type="button" to prevent accidental form submission, ensuring you apply this to
the button that calls setShowBlocklistModal and the other icon-only buttons in
the same block.
- Around line 438-439: The useTheme() hook is being called after early return
paths in the MovieDetails component, violating React's rules of hooks; move the
call to useTheme() (and the derived isAmoledTheme) to the top level of the
component (before any conditional/early return logic) so hooks are always
invoked in the same order—specifically locate the useTheme() invocation and
isAmoledTheme constant in src/components/MovieDetails/index.tsx and relocate
them above the component's early return blocks.

In `@src/components/TvDetails/index.tsx`:
- Around line 1535-1540: Several icon-only native <button> elements (e.g., the
one that calls setShowBlocklistModal(true) and other similar onClick handlers
using EyeSlashIcon, etc., in TvDetails component) lack accessible names and
explicit button types; update each icon-only <button> (those using EyeSlashIcon
and other icons in this file) to include a descriptive aria-label (e.g.,
aria-label="Open blocklist modal") and add type="button" to prevent form
submission side-effects, ensuring each aria-label clearly describes the button
action and matches the associated onClick handler.
- Around line 469-470: The call to useTheme() (and the derived isAmoledTheme)
must be moved to the top of the TvDetails component before any conditional
return paths so hooks are always invoked in the same order; locate useTheme()
and isAmoledTheme in the TvDetails component and relocate them above the
earliest return statements (the early-loading/empty-result returns) so the hook
runs unconditionally on every render.

---

Duplicate comments:
In `@src/components/MovieDetails/index.tsx`:
- Around line 1146-1148: The else branch of the ternary in MovieDetails (the
branch beginning at the ternary else around the AMOLED full-bleed layout)
contains a standalone JSX comment that breaks parsing; move the JSX comment so
it is inside a JSX element (e.g., place the comment as the first child inside
the <div className="relative bg-black">) or convert it to a JS/TS comment above
the return/JSX expression, ensuring the else branch starts immediately with a
valid JSX node (the <div> or a fragment) rather than a standalone comment.

In `@src/components/TvDetails/index.tsx`:
- Around line 1369-1371: The JSX ternary else branch in the TvDetails component
contains a standalone JSX comment between the opening parenthesis and the root
element, causing a parse error; remove or move the JSX comment so the else
branch directly returns the <div className="relative bg-black"> element (or wrap
the comment inside the returned fragment/element) in the ternary expression used
in the TvDetails component to restore valid JSX.

---

Nitpick comments:
In `@src/components/Discover/DiscoverMovies/index.tsx`:
- Around line 111-114: There are two headings rendered: the literal h1 element
and the Header component (both using {title}), which can cause duplicate
announcements for screen readers; update the DiscoverMovies component to render
only one heading at a time by conditionally rendering the h1 or the Header based
on the theme flag (e.g., isAmoledTheme) or ensure the hidden variant uses
display: none in CSS; locate the JSX that contains the <h1
className="discover-movies-amoled-heading ...">{title}</h1> and the
<Header>{title}</Header> and either wrap them in a conditional (render h1 when
isAmoledTheme true else render Header) or change the CSS for
discover-movies-amoled-heading / discover-movies-standard-heading to use
display: none for the inactive state so screen readers won’t see both.

In `@src/components/TitleCard/index.tsx`:
- Around line 377-419: Native <button> elements in the TitleCard component are
missing type="button", which can cause unexpected form submissions if nested in
a form; update each native button (those using onClickWatchlistBtn,
onClickDeleteWatchlistBtn, and the one that calls
setShowBlocklistModal/EyeSlashIcon) to include type="button" so they behave as
non-submitting buttons while leaving the existing Button component instances
unchanged.
- Around line 579-602: The AMOLED request native button (the <button> with
className "title-card-amoled-request-btn" that calls setShowRequestModal in its
onClick) is missing an explicit type, which can cause unintended form
submissions; add type="button" to that button element so it behaves like the
other native button and only triggers setShowRequestModal.
- Around line 536-568: Replace the inline
WebkitLineClamp/display/WebkitBoxOrient/overflow styles in the TitleCard
component: for the title h1 (data-testid="title-card-title") swap those inline
styles for Tailwind's line-clamp utility (e.g., className includes
"line-clamp-3"); for the summary div replace the computed inline WebkitLineClamp
with conditional Tailwind classes (e.g., choose between "line-clamp-3" or
"line-clamp-5" based on showRequestButton and currentStatus/MediaStatus checks)
by building the className string accordingly; remove the redundant inline style
properties (display, overflow, WebkitBoxOrient, wordBreak if already covered)
and ensure the conditional logic that references showRequestButton,
currentStatus and MediaStatus stays intact when computing the className for the
summary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 01c88019-c7f8-4e2d-b1eb-6558dc648394

📥 Commits

Reviewing files that changed from the base of the PR and between fb0c437 and 128ddcd.

📒 Files selected for processing (7)
  • src/components/Common/ButtonWithDropdown/index.tsx
  • src/components/Common/Dropdown/index.tsx
  • src/components/Discover/DiscoverMovies/index.tsx
  • src/components/Discover/DiscoverTv/index.tsx
  • src/components/MovieDetails/index.tsx
  • src/components/TitleCard/index.tsx
  • src/components/TvDetails/index.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/Discover/DiscoverTv/index.tsx
  • src/components/Common/Dropdown/index.tsx

Comment thread src/components/MovieDetails/index.tsx Outdated
Comment thread src/components/MovieDetails/index.tsx
Comment thread src/components/TvDetails/index.tsx Outdated
Comment thread src/components/TvDetails/index.tsx
… buttons

- Move useTheme() hook call before conditional returns in MovieDetails
  and TvDetails to comply with React rules of hooks
- Add type=button and aria-label to all AMOLED icon-only buttons in
  MovieDetails (blocklist, watchlist add/remove) and TvDetails
  (blocklist, watchlist add/remove, report issue, manage series)
@RAFSuNX
Copy link
Copy Markdown
Author

RAFSuNX commented Mar 14, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 14, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
src/components/TvDetails/index.tsx (1)

1369-1372: ⚠️ Potential issue | 🔴 Critical

Fix malformed ternary branch JSX (compile blocker).

At Line 1369, the : ( branch begins with a JSX comment expression at Line 1370 followed by a <div> at **Line 1371`. This branch currently does not parse as a valid single JSX expression in this context, which is causing the parser failures reported at Lines 473/1371/2264.

💡 Proposed fix
-    ) : (
-    {/* ── AMOLED full-bleed layout ── */}
-    <div className="relative bg-black">
+    ) : (
+    <>
+      {/* ── AMOLED full-bleed layout ── */}
+      <div className="relative bg-black">
       ...
-    </div>
+      </div>
+    </>
     )}
#!/bin/bash
# Verify no ternary else-branch starts with a JSX comment expression in TvDetails.
rg -nU '\)\s*:\s*\(\s*\{/\*' src/components/TvDetails/index.tsx

# Expected result after fix: no matches.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/TvDetails/index.tsx` around lines 1369 - 1372, The else branch
of the ternary inside the TvDetails component is malformed because it starts
with a JSX comment expression before the <div> (the branch after ") : ("), which
breaks parsing; fix it by making the else branch a single valid JSX expression —
either remove or move the JSX comment (/* ... */) outside the JSX expression or
wrap the branch in a React fragment (<> ... </>) so the comment does not appear
as the first token, ensuring the block that renders <div className="relative
bg-black"> with <PageTitle title={data.name} /> is a valid single expression in
the ternary.
src/components/MovieDetails/index.tsx (1)

1146-1149: ⚠️ Potential issue | 🔴 Critical

Resolve invalid JSX in AMOLED ternary branch.

At Line 1146, the else branch starts with : (, then a JSX comment at Line 1147 and a <div> at Line 1148. This branch shape is invalid in this context and is the root cause of the parse failures at Lines 442/1148/1719.

💡 Proposed fix
-      ) : (
-      {/* ── AMOLED full-bleed layout ── */}
-      <div className="relative bg-black">
+      ) : (
+      <>
+        {/* ── AMOLED full-bleed layout ── */}
+        <div className="relative bg-black">
         ...
-      </div>
+        </div>
+      </>
       )}
#!/bin/bash
# Verify no ternary else-branch starts with a JSX comment expression in MovieDetails.
rg -nU '\)\s*:\s*\(\s*\{/\*' src/components/MovieDetails/index.tsx

# Expected result after fix: no matches.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/MovieDetails/index.tsx` around lines 1146 - 1149, The JSX
parse error is caused by a JSX comment placed directly at the start of the
ternary else branch in the MovieDetails component (the branch that renders the
AMOLED layout and returns <div className="relative bg-black"> and PageTitle); to
fix it, remove or move the JSX comment out of the expression so the else branch
begins with a valid JSX element (e.g., have the ternary’s else start with the
<div> or wrap the comment above the return/JSX expression), ensuring the
conditional expression in MovieDetails returns a single valid JSX node for the
else branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/MovieDetails/index.tsx`:
- Around line 1146-1149: The JSX parse error is caused by a JSX comment placed
directly at the start of the ternary else branch in the MovieDetails component
(the branch that renders the AMOLED layout and returns <div className="relative
bg-black"> and PageTitle); to fix it, remove or move the JSX comment out of the
expression so the else branch begins with a valid JSX element (e.g., have the
ternary’s else start with the <div> or wrap the comment above the return/JSX
expression), ensuring the conditional expression in MovieDetails returns a
single valid JSX node for the else branch.

In `@src/components/TvDetails/index.tsx`:
- Around line 1369-1372: The else branch of the ternary inside the TvDetails
component is malformed because it starts with a JSX comment expression before
the <div> (the branch after ") : ("), which breaks parsing; fix it by making the
else branch a single valid JSX expression — either remove or move the JSX
comment (/* ... */) outside the JSX expression or wrap the branch in a React
fragment (<> ... </>) so the comment does not appear as the first token,
ensuring the block that renders <div className="relative bg-black"> with
<PageTitle title={data.name} /> is a valid single expression in the ternary.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a8e00780-e2c0-42f7-a437-7120f1bcdb2f

📥 Commits

Reviewing files that changed from the base of the PR and between 128ddcd and 4fe53b6.

📒 Files selected for processing (2)
  • src/components/MovieDetails/index.tsx
  • src/components/TvDetails/index.tsx

RAFSuNX added 2 commits March 15, 2026 21:50
A JSX comment expression followed by a div created two root elements
in the ternary else branch, which is invalid. Remove the comments.
@RAFSuNX
Copy link
Copy Markdown
Author

RAFSuNX commented Mar 15, 2026

Screenshots

Here's a preview of the AMOLED-Strix theme across the main views:

Home / Hero Slider — Cinematic full-bleed backdrop with poster, title, and request button.

Screenshot_20260315-215426

Discover — Series — Violet heading, backdrop blur at top, 2-column card grid.

Screenshot_20260315-215717

Discover — Movies — Same layout as Series with the Movies view.

Screenshot_20260315-215721

Title Cards — Glass-style card with watchlist star, rating badge, and inline Request button.

Screenshot_20260315-215727

Navigation Drawer — Pure black background, violet active state on the current page.

Screenshot_20260315-215734

Movie Detail — Backdrop hero with poster, metadata, and action buttons (blocklist, watchlist, trailer, request).

Screenshot_20260315-215804

@RAFSuNX
Copy link
Copy Markdown
Author

RAFSuNX commented Mar 16, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 16, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/components/MovieDetails/index.tsx (1)

1350-1373: ⚠️ Potential issue | 🟠 Major

Add accessible names to AMOLED icon-only issue/manage controls.

The AMOLED “Report issue” and “Manage movie” controls are icon-only and don’t expose an explicit accessible name in this branch. Please pass aria-label (or an equivalent prop on Button) so screen readers can announce the action.

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

In `@src/components/MovieDetails/index.tsx` around lines 1350 - 1373, The
icon-only Buttons for reporting and managing a movie lack accessible names;
update the Button in the ExclamationTriangleIcon block and the Button wrapping
CogIcon to include an aria-label (or the Button's equivalent prop) using the
corresponding localized strings (e.g., intl.formatMessage(messages.reportissue)
for the report button and intl.formatMessage(messages.managemovie) for the
manage button) so screen readers can announce the actions; ensure the props are
added to the Button components that call setShowIssueModal(true) and
setShowManager(true).
🧹 Nitpick comments (2)
src/components/TvDetails/index.tsx (1)

495-531: Consider extracting shared blocks to reduce branch drift risk.

Both theme branches duplicate substantial functional JSX (modals, cast/recommendation/similar sliders, and several data sections). This will be expensive to maintain and easy to desync. Extract shared subcomponents and keep only layout-specific wrappers theme-conditional.

Also applies to: 1372-1408, 1325-1368, 2217-2261

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

In `@src/components/TvDetails/index.tsx` around lines 495 - 531, The duplicated
modal and manager JSX (BlocklistModal, IssueModal, RequestModal, ManageSlideOver
and their props like tmdbId/data, show handlers, onComplete callbacks,
isUpdating, revalidate, onClickHideItemBtn, closeBlocklistModal, router push)
should be extracted into a single shared subcomponent (e.g.,
TvDetailsSharedModals) that accepts the necessary props (data/tmdbId, show
flags, onCancel/onComplete handlers, isUpdating, revalidate, router handlers)
and is imported into each theme branch; replace the duplicated modal blocks in
the theme-specific files with this shared component so only layout/wrapper
differences remain in the theme branches while all modal logic and callbacks are
centralized.
src/components/MovieDetails/index.tsx (1)

464-1145: Recommend extracting shared MovieDetails sections across theme branches.

The two branch trees duplicate most functional sections (modals, metadata, cast, sliders, external links). Pulling shared logic into subcomponents would reduce regression risk and simplify future updates.

Also applies to: 1149-1716

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

In `@src/components/MovieDetails/index.tsx` around lines 464 - 1145, The
MovieDetails file duplicates large shared UI sections across theme branches
(modals, header, metadata, cast slider, MediaSlider blocks, ExternalLinkBlock)
which should be extracted to reduce duplication; create reusable subcomponents
(e.g., MovieDetailsHeader handling
IssueModal/ManageSlideOver/BlocklistModal/StatusBadge/PlayButton/RequestButton
and manager badge logic, MovieDetailsOverview for
overview/crew/keywords/collection, MovieDetailsFacts for
ratings/release/production/revenue/budget/external links using
ExternalLinkBlock, and MovieDetailsCast for the cast Slider) and replace the
duplicated blocks in src/components/MovieDetails/index.tsx with these components
while preserving props like data, mediaLinks, ratingData, settings, locale,
showManager, showIssueModal, showBlocklistModal, onClickWatchlistBtn,
onClickDeleteWatchlistBtn, revalidate and permission checks (hasPermission) so
behavior (e.g., StatusBadge, RequestButton, PlayButton, MediaSlider calls)
remains identical across branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/components/MovieDetails/index.tsx`:
- Around line 1350-1373: The icon-only Buttons for reporting and managing a
movie lack accessible names; update the Button in the ExclamationTriangleIcon
block and the Button wrapping CogIcon to include an aria-label (or the Button's
equivalent prop) using the corresponding localized strings (e.g.,
intl.formatMessage(messages.reportissue) for the report button and
intl.formatMessage(messages.managemovie) for the manage button) so screen
readers can announce the actions; ensure the props are added to the Button
components that call setShowIssueModal(true) and setShowManager(true).

---

Nitpick comments:
In `@src/components/MovieDetails/index.tsx`:
- Around line 464-1145: The MovieDetails file duplicates large shared UI
sections across theme branches (modals, header, metadata, cast slider,
MediaSlider blocks, ExternalLinkBlock) which should be extracted to reduce
duplication; create reusable subcomponents (e.g., MovieDetailsHeader handling
IssueModal/ManageSlideOver/BlocklistModal/StatusBadge/PlayButton/RequestButton
and manager badge logic, MovieDetailsOverview for
overview/crew/keywords/collection, MovieDetailsFacts for
ratings/release/production/revenue/budget/external links using
ExternalLinkBlock, and MovieDetailsCast for the cast Slider) and replace the
duplicated blocks in src/components/MovieDetails/index.tsx with these components
while preserving props like data, mediaLinks, ratingData, settings, locale,
showManager, showIssueModal, showBlocklistModal, onClickWatchlistBtn,
onClickDeleteWatchlistBtn, revalidate and permission checks (hasPermission) so
behavior (e.g., StatusBadge, RequestButton, PlayButton, MediaSlider calls)
remains identical across branches.

In `@src/components/TvDetails/index.tsx`:
- Around line 495-531: The duplicated modal and manager JSX (BlocklistModal,
IssueModal, RequestModal, ManageSlideOver and their props like tmdbId/data, show
handlers, onComplete callbacks, isUpdating, revalidate, onClickHideItemBtn,
closeBlocklistModal, router push) should be extracted into a single shared
subcomponent (e.g., TvDetailsSharedModals) that accepts the necessary props
(data/tmdbId, show flags, onCancel/onComplete handlers, isUpdating, revalidate,
router handlers) and is imported into each theme branch; replace the duplicated
modal blocks in the theme-specific files with this shared component so only
layout/wrapper differences remain in the theme branches while all modal logic
and callbacks are centralized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a4d7b2a4-713c-42f9-98cf-abbbf1daffe9

📥 Commits

Reviewing files that changed from the base of the PR and between 4fe53b6 and 7968718.

📒 Files selected for processing (3)
  • src/components/MovieDetails/index.tsx
  • src/components/TvDetails/index.tsx
  • src/styles/globals.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/styles/globals.css

@github-actions
Copy link
Copy Markdown

👋 @RAFSuNX, thank you for your contribution!
However, this pull request has been closed because it appears to contain a significant amount of AI-generated code without sufficient human review or supervision.
AI-generated code can often introduce subtle bugs, poor design patterns, or inconsistent styles that make long-term maintenance difficult and reduce overall code quality. For the sake of the project's future stability and readability, we require that all contributions meet our established coding standards and demonstrate clear developer oversight.
This pull request is also too large for effective human review. Please discuss with us on how to break down these changes into smaller, more focused PRs to ensure a thorough and efficient review process. If you'd like to revise and resubmit your changes with careful review and cleanup, we'd be happy to take another look.

@github-actions github-actions Bot closed this Mar 16, 2026
@github-actions github-actions Bot locked as spam and limited conversation to collaborators Mar 16, 2026
@seerr-team seerr-team unlocked this conversation Mar 16, 2026
@RAFSuNX
Copy link
Copy Markdown
Author

RAFSuNX commented Mar 16, 2026

@gauthier-th hi, Sorry if I sound rude. I just wanted to ask whether the PR was rejected mainly because it seemed AI-generated, or if there were other issues with the code as well. I tried to guide the AI with strict rules and standards while generating the code, so I’d really appreciate any specific feedback on what should be improved.

@gauthier-th
Copy link
Copy Markdown
Member

  • This is fully AI-generated, and given the low code quality it has definitely been done without supervision (or not enough supervision or poor supervision)
  • It contains a lot of unrelated changes (why is there that much new components??)
  • We're not going to accept 3000+ changes just to add a new theme

You can't just write thousands of line of code using AI without supervision and expecting everything to be fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants