Skip to content

Conversation

@Klakurka
Copy link
Member

@Klakurka Klakurka commented Nov 29, 2025

Related to #238

Description

If somebody sets the tertiary color to white (or something close), it makes the QR code and other text unusable.

Test plan

Set a button's tertiary color to be very light (eg. #FFFFFF) and see how the dialog goes into a dark mode so that the QR code and misc text is actually visible.

Summary by CodeRabbit

  • New Features

    • Widget now adapts colors dynamically to light/dark themes across containers, QR codes, text, inputs, buttons, icons, and footer for improved visual consistency.
    • Improved color detection and normalization so theme-aware styling is more accurate across varied color inputs.
  • Tests

    • Test mocks adjusted to better preserve actual utilities during testing.

✏️ Tip: You can customize this high-level summary in your review settings.

Greptile Overview

Greptile Summary

This PR implements automatic dark mode for the PayButton widget when the tertiary color is too light (e.g., white or near-white), addressing the visibility issue where QR codes and text become unreadable.

Key Changes:

  • Created color.ts utility with WCAG 2.0 compliant luminance calculation to detect light colors
  • Applied conditional dark mode styling across 10+ UI elements (backgrounds, borders, text, inputs, QR code)
  • Properly memoized isDarkMode calculation to avoid unnecessary recalculations
  • Fixed test mocks to include actual utility exports

Issues Found:

  • SSR compatibility issue: document.createElement will crash in server-side rendering environments

Confidence Score: 3/5

  • Safe to merge after fixing the SSR compatibility issue in the color utility
  • The implementation is well-structured with proper memoization and uses standard WCAG formulas, but has a critical SSR bug that will crash the application in server-side environments. The fix is straightforward (add typeof document !== 'undefined' check).
  • Pay close attention to react/lib/util/color.ts - needs SSR compatibility fix before deployment

Important Files Changed

File Analysis

Filename Score Overview
react/lib/util/color.ts 3/5 Added color utility functions for luminance calculation and dark mode detection using WCAG 2.0 formula; missing SSR safety checks for document usage
react/lib/components/Widget/Widget.tsx 4/5 Applied dark mode styling conditionally across UI elements based on tertiary color luminance; properly memoized isDarkMode calculation

Sequence Diagram

sequenceDiagram
    participant User
    participant Widget
    participant useTheme
    participant darkMode
    participant normalizeColorToHex
    participant getLuminance
    participant UI as UI Components

    User->>Widget: Render with theme.tertiary color
    Widget->>useTheme: Get theme palette
    useTheme-->>Widget: theme.palette.tertiary
    Widget->>darkMode: Calculate isDarkMode(tertiary)
    darkMode->>normalizeColorToHex: Normalize color to hex
    normalizeColorToHex->>normalizeColorToHex: document.createElement('canvas')
    normalizeColorToHex->>normalizeColorToHex: ctx.fillStyle = color
    normalizeColorToHex-->>darkMode: Hex color string
    darkMode->>getLuminance: Calculate luminance(hex)
    getLuminance->>getLuminance: Apply WCAG 2.0 formula
    getLuminance-->>darkMode: Luminance value (0-1)
    darkMode->>darkMode: Compare luminance > 0.5
    darkMode-->>Widget: isDarkMode boolean
    Widget->>Widget: useMemo caches result
    Widget->>UI: Apply conditional styles
    alt isDarkMode = true
        UI->>UI: Dark backgrounds (#2a2a2a, #1a1a1a)
        UI->>UI: Light text colors
        UI->>UI: Dark QR background
    else isDarkMode = false
        UI->>UI: Light backgrounds (#f5f5f7, #fff)
        UI->>UI: Dark text colors
        UI->>UI: Light QR background
    end
    UI-->>User: Render readable widget
Loading

@Klakurka Klakurka added this to the Phase 3 milestone Nov 29, 2025
@Klakurka Klakurka requested a review from chedieck November 29, 2025 08:13
@Klakurka Klakurka self-assigned this Nov 29, 2025
@Klakurka Klakurka added bug Something isn't working enhancement (UI/UX/feature) New feature or request labels Nov 29, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 29, 2025

Walkthrough

Adds a new color utility module for color normalization and WCAG luminance-based darkMode detection, re-exports it from util index, updates the Widget component to use darkMode on the theme tertiary color to switch many UI styles, and adjusts a test mock to preserve actual util exports.

Changes

Cohort / File(s) Change Summary
Color Utility Module
react/lib/util/color.ts
New module: hexToRgb, normalizeColorToHex, getLuminance, and darkMode functions implemented (canvas-based color normalization and WCAG luminance calculation).
Util Re-exports
react/lib/util/index.ts
Re-exported the new color utilities via export * from './color'.
Widget Theming
react/lib/components/Widget/Widget.tsx
Replaced static color choices with darkMode(...)-driven isDarkMode derived from theme.palette.tertiary; multiple containers, QR rendering props, text, icon stroke, link, donation, and footer styles now conditionally use dark/light values. Dependency array for computed styles updated to include isDarkMode.
Tests: util mock
react/lib/tests/components/Widget.test.tsx
Adjusted jest mock to preserve actual ../../util exports (using ...jest.requireActual('../../util')) while overriding specific keys.
Manifest
package.json
Mentioned in manifest file list (no described functional change in summary).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify WCAG luminance implementation (gamma linearization and coefficients).
  • Confirm normalizeColorToHex handles transparent and non-hex CSS colors robustly across browsers.
  • Check all Widget style branches for consistent isDarkMode usage and complete dependency arrays.
  • Review test mock adjustments to ensure no unintended shadowing of util exports.

Poem

🐰 I hopped through hues both bright and stark,
Turning tertiary tones into a spark,
I measured light with careful ears,
Switched the theme between shadows and cheers,
Now the Widget twinkles in day and dark. ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing dark mode when the tertiary color is light, which directly addresses the primary objective of improving QR code and text readability.
Description check ✅ Passed The description includes all required template sections: issue reference (#238), clear problem statement, and actionable test plan. The author explains the motivation and provides concrete testing steps.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/dark-mode-based-on-tertiary-color

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.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@Klakurka Klakurka marked this pull request as draft November 29, 2025 08:16
Copy link
Contributor

@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

🧹 Nitpick comments (4)
react/lib/components/Widget/Widget.tsx (2)

353-365: Dark-mode styling based on isLightTertiary looks coherent

The conditional styling for:

  • root/header backgrounds ('#2a2a2a' / '#3a3a3a' vs light backgrounds),
  • QR container background/border,
  • copy text container & text shadow,
  • footer and SideShift link colors, and
  • QRCodeSVG bgColor,

all consistently flip to a darker shell when theme.palette.tertiary is light, while keeping text/QR foreground as theme.palette.tertiary. This should address the “white tertiary makes QR unreadable” issue without breaking the default light theme.

One minor note: isLightTertiary already derives purely from theme.palette.tertiary, so the useMemo here is more an optimization than a necessity; you could inline isLightColor(theme.palette.tertiary) and drop the extra hook if you prefer less hook surface.

Also applies to: 384-399, 409-432, 484-485, 1033-1047, 1064-1071


1350-1352: Donation UI colors correctly adapt, but rely on hex/named palette values

The donation heart icon stroke and the donation rate input/label colors now branch on isLightTertiary, which keeps them visible against both darkened and light backgrounds.

Be aware this relies on theme.palette.tertiary being a hex or named color that isLightColor can parse; if you ever move to CSS variables or rgb()/hsl() strings in the palette, isLightColor will currently treat them as “dark” (luminance 0). In that case you’d want to extend the color utility to handle those formats.

Also applies to: 1381-1389, 1399-1402

react/lib/util/color.ts (2)

5-150: Named-color map and normalization are fine but limited to names and hex

The colorNameToHex table plus normalizeColorToHex give you robust coverage for common CSS named colors and 3/6‑digit hex (with or without #). That’s sufficient for typical theme palette values.

Right now anything that isn’t a known name or hex (e.g. rgb(...), hsl(...), var(--color) or 4/8‑digit hex with alpha) will normalize to an invalid hex and end up with luminance 0. That’s a safe failure mode, but if you ever store such formats in theme.palette.*, isLightColor will misclassify them as “dark”. Consider documenting this or extending normalization when/if those formats appear in the theme.

Also applies to: 178-190


220-227: isLightColor threshold is reasonable; consider centralizing if reused

Wrapping getLuminance with a default threshold = 0.5 is straightforward and produces intuitive results (dark vs light). If you expect multiple call sites with different thresholds, consider exporting a named constant (e.g. DEFAULT_LIGHT_THRESHOLD) so you don’t end up with scattered magic numbers.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3356668 and 7fb4aa6.

📒 Files selected for processing (3)
  • react/lib/components/Widget/Widget.tsx (12 hunks)
  • react/lib/util/color.ts (1 hunks)
  • react/lib/util/index.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
react/lib/components/Widget/Widget.tsx (2)
react/lib/util/color.ts (1)
  • isLightColor (225-227)
docs/darkmode.js (1)
  • theme (39-39)
🪛 GitHub Actions: Run tests
react/lib/util/color.ts

[error] 1-1: Test suite failed: Unhandled exception in Widget.test.tsx caused by runtime error in Widget component.

react/lib/components/Widget/Widget.tsx

[error] 326-326: Uncaught TypeError: (0 , util_1.isLightColor) is not a function. Ensure isLightColor is exported from the util module and properly imported in Widget.tsx.

react/lib/util/index.ts

[error] 1-1: Test suite failed: Unhandled exception in Widget.test.tsx caused by runtime error in Widget component.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run build
🔇 Additional comments (2)
react/lib/util/color.ts (1)

155-172: Luminance computation matches WCAG 2.0 and looks correct

hexToRgb correctly handles 3- and 6-digit hex and cleanly returns null for invalid values. getLuminance:

  • Normalizes the input color,
  • Converts to sRGB,
  • Applies gamma correction, and
  • Uses the WCAG coefficients (0.2126, 0.7152, 0.0722),

so the luminance result will be in [0, 1] as expected. This is appropriate for the light/dark decision logic.

Also applies to: 197-218

react/lib/components/Widget/Widget.tsx (1)

44-45: I'll help you verify the concerns in this review comment. Let me start by exploring the codebase to confirm the issues with the isLightColor import and util exports.
<function_calls>

#!/bin/bash

First, let's locate Widget.tsx and understand the file structure

echo "== Finding Widget.tsx =="
fd "Widget.tsx" -t f

echo
echo "== Checking repo structure =="
git ls-files react/lib/components/Widget/ 2>/dev/null | head -20


</function_calls>
<function_calls>

#!/bin/bash

Let's look at the util barrel and its exports

echo "== Util barrel structure =="
fd "util" -t d react/lib/ | head -10

echo
echo "== Check for index files in util =="
fd "index.(ts|tsx|js|jsx)" react/lib/util/ -o react/lib/utils/ 2>/dev/null | head -10


</function_calls>
<function_calls>

#!/bin/bash

Search for isLightColor definition and exports

echo "== Search for isLightColor definition =="
rg -n "isLightColor" --type ts --type tsx -A 2 -B 2

echo
echo "== Search for color utility files =="
fd "color" -t f react/lib/util/


</function_calls>
<function_calls>

#!/bin/bash

Check util directory structure more closely

echo "== Contents of react/lib/util =="
ls -la react/lib/util/ 2>/dev/null || echo "Directory not found at react/lib/util"

echo
echo "== Try alternate paths =="
fd "util" -t d | grep -E "(react|lib)" | head -10


</function_calls>
<function_calls>

#!/bin/bash

List all TypeScript/TSX files in the repo to understand structure

echo "== Repository structure =="
git ls-files | grep -E ".(ts|tsx)$" | head -30


</function_calls>
<function_calls>

#!/bin/bash

Look at Widget.tsx content around lines 44-45

echo "== Widget.tsx content (lines 1-100) =="
find . -name "Widget.tsx" -type f -exec cat -n {} ; | head -100


</function_calls>
<function_calls>

#!/bin/bash

Search more broadly for isLightColor

echo "== Broad search for isLightColor =="
rg "isLightColor" -n

echo
echo "== Search for color-related exports =="
rg "export.*color" -n --type ts --type tsx | head -20


</function_calls>
<function_calls>

#!/bin/bash

Check for Jest mock files

echo "== Search for mocks directories =="
find . -type d -name "mocks"

echo
echo "== Search for jest.mock calls =="
rg "jest.mock" -n -A 3 -B 1


</function_calls>

@Klakurka Klakurka marked this pull request as ready for review November 29, 2025 19:55
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

if (!trimmed) return null;

// Use canvas to parse any CSS color value
const ctx = document.createElement('canvas').getContext('2d');
Copy link

Choose a reason for hiding this comment

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

logic: SSR compatibility: document is not available in server-side environments

Suggested change
const ctx = document.createElement('canvas').getContext('2d');
const ctx = typeof document !== 'undefined' ? document.createElement('canvas').getContext('2d') : null;
Prompt To Fix With AI
This is a comment left during a code review.
Path: react/lib/util/color.ts
Line: 34:34

Comment:
**logic:** SSR compatibility: `document` is not available in server-side environments

```suggestion
  const ctx = typeof document !== 'undefined' ? document.createElement('canvas').getContext('2d') : null;
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@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: 0

🧹 Nitpick comments (1)
react/lib/util/color.ts (1)

27-40: Guard normalizeColorToHex for non-browser / SSR environments

normalizeColorToHex assumes document.createElement('canvas').getContext('2d') is available. In environments without a DOM or canvas implementation (SSR, some test runners), document or getContext can throw before the ctx null-check, which would bubble up through getLuminance/darkMode.

To make this utility safer without changing behavior in the browser, consider an early guard and a defensive getContext call:

 export const normalizeColorToHex = (color: string): string | null => {
   if (!color) return null;
 
   const trimmed = color.trim();
   if (!trimmed) return null;
-  
-  // Use canvas to parse any CSS color value
-  const ctx = document.createElement('canvas').getContext('2d');
-  if (!ctx) return null;
-  
-  ctx.fillStyle = trimmed;
-  // The browser normalizes the color to a hex string (or rgb() for transparent colors)
-  return ctx.fillStyle;
+
+  // Use canvas to parse any CSS color value; bail out if DOM/canvas are unavailable
+  if (typeof document === 'undefined') return null;
+
+  const canvas = document.createElement('canvas');
+  const ctx = canvas.getContext && canvas.getContext('2d');
+  if (!ctx) return null;
+
+  ctx.fillStyle = trimmed;
+  // The browser normalizes the color to a hex string (or rgb()/rgba() for transparent colors)
+  return ctx.fillStyle;
 };

That way darkMode simply returns false (via luminance 0) when no DOM/canvas is available, instead of throwing.

If you expect this code to run under SSR or in non-jsdom test environments, it’s worth quickly confirming how document.createElement('canvas').getContext('2d') behaves there and adjusting the guard accordingly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb4aa6 and a2685bb.

📒 Files selected for processing (3)
  • react/lib/components/Widget/Widget.tsx (12 hunks)
  • react/lib/tests/components/Widget.test.tsx (1 hunks)
  • react/lib/util/color.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • react/lib/components/Widget/Widget.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run build
🔇 Additional comments (3)
react/lib/tests/components/Widget.test.tsx (1)

8-47: Partial util mock correctly preserves real exports

Spreading jest.requireActual('../../util') and then overriding specific functions is a solid pattern here: it keeps new utilities (like the color helpers/dark mode) using their real implementations while safely stubbing network and formatting helpers. This should make the tests more robust to future additions to ../../util without accidentally auto-mocking them.

If you want extra certainty, you can quickly log Object.keys(jest.requireActual('../../util')) in a local throwaway test to confirm all expected exports (including the new color utilities) are visible under this mock shape.

react/lib/util/color.ts (2)

4-21: WCAG luminance helper hexToRgb implementation looks correct

The hex parsing logic here is clean and idiomatic: stripping the leading #, expanding 3‑digit hex to 6‑digit, validating via regex, and returning parsed RGB or null on invalid input all look good and align with the intended usage in getLuminance.


47-77: WCAG luminance and darkMode thresholding are implemented correctly

The getLuminance function correctly follows the WCAG 2.0 relative luminance formula (sRGB normalization, gamma correction, and the 0.2126/0.7152/0.0722 weighting), and darkMode cleanly wraps it with a configurable threshold (default 0.5) to flip into a dark UI when the input color is light. This matches the PR goal of triggering dark mode when the tertiary color is very light (e.g., white).

@chedieck chedieck merged commit 0ed8c3b into master Dec 5, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement (UI/UX/feature) New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Change bg colour when tertiary theme colour is set to white (as it's no longer visible)

3 participants