Skip to content

Fix: floating operation pill stays theme-aware in Light mode (#213) - #215

Merged
StudioNirin merged 7 commits into
StudioNirin:mainfrom
DeLo1585:fix/light-theme-pill-213
Sep 29, 2026
Merged

StudioNirin merged 7 commits into
StudioNirin:mainfrom
DeLo1585:fix/light-theme-pill-213

Conversation

@DeLo1585

@DeLo1585 DeLo1585 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #213
Source review / writing / testing done together with Claude Sonnet 5 (Anthropic).

What changed

The floating operation-status pill (#global-operation-banner .di-pill) previously had a hard-coded black background and white text/decoration colors throughout, both in its compact and expanded (Tier 2/3, detail-log) states. The existing Light theme override only softened the drop shadow, intentionally keeping the pill black (see the removed comment "Pill stays dark in light mode").

This PR makes the pill genuinely theme-aware for Light mode instead, while leaving Dark mode pixel-identical to before.

Approach

Introduced three CSS custom properties, scoped to #global-operation-banner (the pill's outer, always-present container):

#global-operation-banner {
    --di-on: 255,255,255;   /* RGB triplet used as rgba(var(--di-on), X) */
    --di-bg: #000;
    --di-track-bg: #1a1a1a;
}

Dark theme keeps these at their original literal values, so nothing changes there. [data-theme="light"] #global-operation-banner overrides them to a light surface (var(--plex-bg-card)) and dark-on-light text, matching the rest of the app's existing Light theme palette:

[data-theme="light"] #global-operation-banner {
    --di-on: 0,0,0;
    --di-bg: var(--plex-bg-card);
    --di-track-bg: rgba(0,0,0,0.08);
}

All previously hard-coded rgba(255,255,255,X) / #fff / #1a1a1a occurrences inside the pill (label, muted metric, progress bar/track, divider, expand-hint, detail log rows, queue/countdown text, etc. — both compact and expanded states) now reference these tokens instead, in both custom.css and the few inline style="..." occurrences in global_operation_banner.html that CSS classes alone couldn't reach.

Left untouched on purpose:

  • All --accent operation-state colors (.di-pill--caching, --evicting, --error, etc.) — caching/evicting/error color coding is identical in both themes.
  • .di-error-badge keeps fixed white text, since its background is always red regardless of theme.

Testing

Manually tested on a running v3.2.0 container (Docker/Unraid) by swapping in the patched files via docker cp and comparing both themes before/after:

  • Light theme: pill now renders as a white card with dark text, consistent with the rest of the Light theme (idle, caching-in-progress with expanded log detail panel, and completed states all checked).
  • Dark theme: confirmed pixel-identical to the current behavior (black pill, white text)

Screenshots:

grafik
grafik
grafik
grafik

@StudioNirin

Copy link
Copy Markdown
Owner

@Brandon-Haney

I did some quick tests and looked through it and it seems to work. Does this interfere with anything you might be working on?

If not, ill merge it. I always use dark mode so I didnt even realise this was an issue haha

@DeLo1585

Copy link
Copy Markdown
Contributor Author

Thanks for the quick reply. I haven't worked on any other issues or FRs in here so far.

This PR actually only involves those two files.

@Brandon-Haney

Copy link
Copy Markdown
Collaborator

@StudioNirin hold off on the merge for a moment, it does interfere, though not in the way the question was meant.

@DeLo1585 the theme change itself looks good and the approach is the right one, I'll come back to that. The problem is the branch. It's based on current main, but the two files were committed as their v3.2.0 versions with the token change applied on top, so the diff carries everything that's landed in those files since the release as deletions. That's 276 lines removed against 86 added. Merging it as is would take out all of the Recently Added styles, the share-mismatch warning, the extras badge and bytes-first progress line from #210, and the group id keying that stops two groups for the same show from colliding in the banner. Easy to miss when you've been testing by copying the files into a running container, since that container is on 3.2.0 too.

The fix is small. Check out main, reapply only the token block and the rgba(255,255,255,X) to rgba(var(--di-on),X) swaps, and leave the rest of the two files alone. The diff should then be additions and one-line replacements with no removed blocks. If it's easier, I can push a rebased version of your change to a branch and you can pull it into the PR.

On the design side, some background. The pill being dark in light mode was a deliberate call back in February, not a leftover. Light theme came later than the rest of the UI, we tried a light pill at the time and it didn't look right, so we kept it dark with a stronger border and shadow. I'm not against a light pill, but the pill's status colours are hard coded literals that were tuned for a black surface, and they don't hold up on white. In your screenshots the Plex orange idle accent clashes with the card, and the muted text is going to be hard to read, since a lot of the swaps land at 0.15 to 0.25 black on white. The light blue countdown timer is a fixed colour too.

If you want to keep going with the light surface, I'd suggest:

  • Under [data-theme="light"] #global-operation-banner, override the --accent values per state with the semantic tokens from plex-theme.css (--plex-success, --plex-warning, --plex-error, --plex-info) since those already have light theme values, rather than reusing the literals.
  • Drop or heavily reduce --accent-glow in light mode, the halo reads as a smudge on white.
  • Raise the muted alphas for light. Anything at 0.15 to 0.3 white on black needs to be roughly 0.45 to 0.6 black on white to be legible.
  • Give the countdown timer and any other fixed colours a light override too.

If that ends up being a lot of tuning, the other option the issue itself raised is fine with me: keep the pill as a deliberate dark element in light mode and give it a stronger border so it reads as intentional. Either way, thanks for digging into this, light mode has had less attention than it deserves.

@StudioNirin

Copy link
Copy Markdown
Owner

Ahh I see, good catch @Brandon-Haney I didnt think to look for that

@DeLo1585

DeLo1585 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Yeah, I used the v3.2.0 source zip, that explains it, wasn't on current main.

I redo the changes against the current branch (with Claude's Sonnet help) and then update this PR and let you both know once it's ready

first adjustments results were:

  • Diff is now 45 removed / 120 added in custom.css
  • a clean 13/13 in the template,
  • all one-line replacements,
  • no removed blocks (Recently Added, the share-mismatch warning, extras badge, bytes-first progress, and the group-id keying are all untouched and still present).

@DeLo1585

DeLo1585 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

(Worked with Claude Sonnet 5 on this patch — hoping nothing got scrambled or out of sync this time around!)

I've rebased against current main and reapplied just the token block + rgba swaps on top, nothing else touched.

Diff is now 45 removed / 120 added in custom.css and a clean 13/13 in the template, all one-line replacements, no removed blocks (Recently Added, the share-mismatch warning, extras badge, bytes-first progress, and the group-id keying are all untouched and still present).

On the design points, I went with tuning it rather than falling back to a dark pill, using your suggestions directly:

  • Each .di-pill--* state now overrides --accent to the semantic tokens (--plex-success/-warning/-error/-info) in Light theme instead of the literals. Idle keeps the Plex orange as-is, since that's already used elsewhere in Light theme.
  • To keep the icon background circle and spinner ring consistent with those updated state colors in Light theme, --accent-dim is now derived dynamically via color-mix(in srgb, var(--accent) 15%, transparent) per state, matching the pattern used elsewhere in the app so the dim tint stays in sync with whichever semantic token is active.
  • --accent-glow is set to transparent in Light theme rather than tuned down, per your "drop it" option, the pulse halo didn't read well on white at any alpha I tried.
  • Replaced the flat rgba(var(--on), X) approach for actual text with discrete tiers (--di-t15 through --di-t70, named by their dark-mode alpha) that get boosted independently in Light theme, e.g. what was 0.25 white-on-black is 0.55 black-on-white now, not a symmetric mirror. That was the fix for the "Next run" text being hard to read that you flagged.
  • The three fixed light-blue spots (queue badge, active-extras tag, countdown timer) now go through a --di-info-text token tied to --plex-info.

Pushed both files to the same branch. Let me know if the contrast tiers need further adjustment, I picked the boosted values based on your 0.45–0.6 guidance but didn't have a way to measure actual contrast ratios here.

@DeLo1585

DeLo1585 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Small correction to my last comment, the --accent-dim description doesn't quite match what's actually in the pushed diff.

I originally tried deriving it dynamically as color-mix(in srgb, var(--accent) 15%, transparent), but went with explicit per-state overrides instead (color-mix(in srgb, var(--plex-success) 18%, transparent) etc., one line per .di-pill--* variant, same pattern as the --accent overrides above them), so it's tied directly to the semantic tokens rather than re-deriived from --accent at render time. Diff stats are 45 removed / 134 added in custom.css (not 120, that was before this part), template is still a clean 13/13.

Functionally the result is the same either way, tint stays in sync with whichever state/token is active, this is just the version that's actually in the branch now. Sorry for the mismatch in the writeup.

so @StudioNirin @Brandon-Haney its ready for review the updates 2 files

@Brandon-Haney

Brandon-Haney commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Went through the rebased branch and it's clean now. Merge base is current main, every removed line is one of the literal colour swaps, the template is 13 one-line replacements, and all the code I was worried about is untouched. Dark theme resolves to the same values as before. Nice work turning that around quickly.

I put the pill states side by side in both themes (dark left, light right) and measured the light side. All the text tiers you boosted come out above 4.5:1, most well above, so the tiering approach did its job. Click the image for full size, or open dark and light separately.

Pill states rendered with this branch, dark theme on the left and light theme on the right

Two spots are still short:

  • The idle countdown value ("in 3h 12m") is the Plex orange at 70% on the white card, which comes out around 1.8:1. That's the one that reads washed out. I'd give the value itself a text tier in light mode, --di-t60 or so, and leave the orange on the clock icon where it works.
  • The "Start Now" button in the maintenance countdown has an inline background: var(--plex-info); color: #000. In light mode --plex-info is a dark blue, so that's black on dark blue at under 4:1. That one predates your change, but since you already route the fixed blues through --di-info-text, a matching token for the button text (black in dark, white in light) would clear it.

Happy to approve once those two are in. @StudioNirin from my side this is good to merge after that.

Brandon-Haney added a commit to Brandon-Haney/PlexCache-R that referenced this pull request Sep 25, 2026
@DeLo1585

Copy link
Copy Markdown
Contributor Author

@Brandon-Haney Thank you for your Honest expertise and the comparison screenshots. This overview is truly fantastic, and helps me a lot. I will implement this change within the next 12 hours 🙏👌💪

@StudioNirin

Copy link
Copy Markdown
Owner

@Brandon-Haney Thank you for your Honest expertise and the comparison screenshots. This overview is truly fantastic, and helps me a lot. I will implement this change within the next 12 hours 🙏👌💪

Looks good, buzz me when youve made that change and ill merge. 🫡

@DeLo1585

DeLo1585 commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Both fixed.

  • The countdown value now runs through a --di-idle-value token, color-mix(...var(--plex-orange) 70%...) stays as-is in dark, switches to var(--di-t60) in light. Clock icon still just var(--plex-orange), untouched.

  • For the button, added --di-btn-text-on-accent (#000 dark / #fff light) and swapped it in on "Start Now".

@Brandon-Haney

While Sonnet5 was at it and went looking for anything else with the same shape of bug and found one more:

"Resume" in the Queue Paused state had the identical setup

  • background: var(--plex-warning); color: #000, and --plex-warning goes darker in light mode too (#ff9800 → #c77c02), same contrast drop. Same token fixes both now.

Testing on my own container in a bit, but happy for you to take another pass whenever, in case something else is still off that I haven't caught.

Don't have your nice side-by-side comparison setup though, so I'm mostly eyeballing it :D

Brandon-Haney added a commit to Brandon-Haney/PlexCache-R that referenced this pull request Sep 28, 2026
Brandon-Haney added a commit to Brandon-Haney/PlexCache-R that referenced this pull request Sep 28, 2026
@Brandon-Haney

Copy link
Copy Markdown
Collaborator

Went through e8e89d9 again with the pill states rendered side by side in both themes, and I want to make the case for a few more light-theme tweaks before this merges. The direction is right and I'm not looking to reopen it, these are about keeping things the pill did well on black. Implementation details for each are at the bottom.

1. The state colour disappears. On black, the accent-tinted border was how you told caching from restoring from a failure without reading the label. In light mode the running pills keep that border at an 18% tint, which is close to invisible on white, and the finished and failed pills still use their dark-theme literals at 30%, so every state comes out as a plain white pill. Keying the border off each state's --accent at a stronger tint brings the cue back for all eight states with one rule.

Eight pill states: dark, PR light, proposed light

2. Separation over cards. The pill floats over whatever is at the top of the page, which in light mode is usually a white card. Its shadow is the same weight the cards use, so it reads as part of the card underneath. The theme already has --plex-shadow-elevated for floating elements. With that plus the stronger border it sits on top of the card again.

Pill over a white card: main, PR, proposed

Compact caching pill: main, PR, proposed

3. Resume. The white text fixed one thing and exposed another. White on the light warning orange measures about 3.3:1, and the bigger issue is the fill: that token was darkened so it works as text on white, and darkening a saturated orange pushes it toward brown, which clashes next to the Plex orange on Run Now. I'd drop the inline background and colour on Resume entirely and let it be a normal .di-btn--primary. Black on Plex orange is 9.4:1 in both themes and it matches the other primary actions. Row C below. Start Now keeps your token, it's the right call there.

Four Resume treatments in light and dark

4. Chevron (optional). The expand chevron shares a text tier so it went from 20% to 55%. It's decorative and doesn't need a text ratio, and at 55% it's heavier than in dark. 30% keeps it quiet. Take it or leave it.

Chevron at 55% vs 30%

One note on the text tiers so it doesn't look like I'm ignoring it: the sidecar rows, sizes and "+ N more" all land at the same weight now, where dark has three visible levels. Anything lighter at those sizes fails 4.5:1, so I think you made the right trade. If it bothers anyone later, depth can come back through weight rather than alpha.


How to implement

All of this is light-theme only. Nothing in the dark theme changes, and none of it touches the --di-t* tiers or the --accent remaps you already have.

web/static/css/custom.css

a) In the existing block at line 1254:

[data-theme="light"] .di-pill {
    border: 1px solid var(--plex-border);
    box-shadow: 0 4px 20px rgba(0,0,0,0.12);
}

replace the box-shadow line with:

    box-shadow: var(--plex-shadow-elevated), 0 1px 2px rgba(0,0,0,0.10);

--plex-shadow-elevated is defined in plex-theme.css for both themes, so no new token is needed. Keep the border line as it is; the rule below only changes the colour, so the 1px border on idle pills stays.

b) Append this after the --accent-glow: transparent block that ends at line 1300:

/* State colour on the pill border. Running pills carry .di-glow; finished
   and failed pills set their own border in the dark rules above, so list
   them here too. Keys off each state's --accent, which is already remapped
   to the semantic tokens for light mode. */
[data-theme="light"] .di-glow,
[data-theme="light"] .di-pill--completed,
[data-theme="light"] .di-pill--completed-errors,
[data-theme="light"] .di-pill--error,
[data-theme="light"] .di-pill--disconnected,
[data-theme="light"] .di-pill--dry-run {
    border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}

/* The expand chevron is decorative, not text, so it does not need a text
   tier. Keep it as quiet as it is in dark. */
[data-theme="light"] .di-pill__compact::after {
    border-right-color: rgba(0,0,0,0.3);
    border-bottom-color: rgba(0,0,0,0.3);
}

Two things to know so the result matches the renders:

  • Only set border-color, not border. The dry-run pill has a dashed border and .di-glow sets its width, and a border shorthand here would override both.
  • The rule at line 662 (.di-pill.di-expanded { border-color: ... !important }) still wins when a pill is expanded, so the accent border shows on the compact pill and drops away when it opens. That is the same behaviour as dark and is intended.

web/templates/components/global_operation_banner.html

At line 1369, the Resume button in the queue-paused state:

<button class="di-btn di-btn--primary" style="background: var(--plex-warning); color: var(--di-btn-text-on-accent); height: 28px;"

becomes:

<button class="di-btn di-btn--primary" style="height: 28px;"

.di-btn--primary already gives it the Plex orange fill and black text in both themes. Leave the Start Now button at line 1282 exactly as you have it, with --di-btn-text-on-accent; that token is still doing the right job there.

Happy to push all of this to your branch if that's easier than another round trip, just say so.

@StudioNirin

Copy link
Copy Markdown
Owner

@DeLo1585 @Brandon-Haney

Im keeping an eye on this, but I'm going to leave the details down to you two as i literally never use light mode on any app haha. I get migraines so i have to avoid any apps without a dark mode 😄

Ill keep following the progress though, and once youre happy with it ill merge and then probably push a new version.

@DeLo1585

DeLo1585 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks @Brandon-Haney — I’m happy to take all of these changes.

1. State colour: Absolutely yes. I really like the coloured border in your proposed version — actually, I love it. It brings back the visual distinction between the different states without making the pills feel too heavy. I’d definitely like to keep that.

2. Shadow / separation: Also yes. I’m happy to use --plex-shadow-elevated here. The stronger separation from the cards looks right to me, so please go ahead with that as well.

3. Resume: I agree with your proposal here too. The amber fill with black text looks the best to me, and I’d be very happy to make that change. I’m also fine with the small dark-theme change that comes with using the regular .di-btn--primary styling. The improved consistency with the other primary actions is worth it to me.

4. Chevron: There seems to be a small technical discrepancy in the reasoning here. The resting chevron is currently using

1.5px solid rgba(var(--di-on),0.2) line 678 / 679

so 20% white, and it isn’t tied to a text tier .di-pill__compact. The hover state is separate at 45%

border-color: var(--di-t45) line 686.

So from a technical/code perspective, I wouldn’t describe this as correcting a 55% value.

That said, looking at your render, I actually really like your proposed version visually. It looks very clean and gives the chevron just a little more presence without making it too strong. So even though I wouldn’t base the change on the technical reasoning above, I’m absolutely happy with the 30% version if it looks like your render in the actual UI.

So yes — please go ahead and push all four changes to my branch. Thanks again for taking the time to go through the states and put together the comparison renders. I really like where this is heading.

Give each pill state its accent-coloured border in light mode, using
the semantic tokens the states already map to, so caching, restoring,
maintenance, dry runs and failures read at a glance the way they do on
the dark pill. Use the theme's elevated shadow so the pill sits above
the page card it floats over, and make Resume a standard primary button
to match Run Now in both themes. The resting expand chevron gets a
little more presence on white.
@Brandon-Haney

Copy link
Copy Markdown
Collaborator

Pushed bef8802 to your branch with all four. Same two files, nothing else touched, and I re-rendered every state from the branch afterwards to confirm it matches the comparisons above.

On the chevron, I had the numbers wrong in my write-up. The resting one is a raw 20% of the foreground rather than a tier, so in light it was 20% black and this takes it to 30%, a small step up rather than down from 55%. The CSS comment says that now. Hover at --di-t45 is untouched.

Also trimmed the comment on --di-btn-text-on-accent since it only serves Start Now now.

@StudioNirin from my side this is ready to merge once DeLo1585 has had a look on their container.

@DeLo1585 DeLo1585 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @Brandon-Haney, and thanks for correcting the chevron note.

I pulled these two files from bef8802 into my container and went through the states in both themes. It looks great in light, the coloured borders make the states readable again, and the elevated shadow separates the pill from the cards nicely. Resume now matches Run Now, and dark looks the same as before apart from that button.

From my side this is good to merge. @StudioNirin whenever you have time for the code owner review.

@StudioNirin
StudioNirin merged commit 3a8207c into StudioNirin:main Sep 29, 2026
2 checks passed
@DeLo1585
DeLo1585 deleted the fix/light-theme-pill-213 branch September 29, 2026 00:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Floating status pill (global operation banner) stays hard-coded black in Light theme

3 participants