Skip to content

feat: Add Light Mode Switching#658

Open
TechHutTV wants to merge 18 commits into
mainfrom
ui-accessibility
Open

feat: Add Light Mode Switching#658
TechHutTV wants to merge 18 commits into
mainfrom
ui-accessibility

Conversation

@TechHutTV

@TechHutTV TechHutTV commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an initial light theme to the dashboard, which is currently dark-only. The core idea is to make the nb-gray color scale theme-aware: instead of hard-coded hex values, the palette is driven by CSS variables that swap between a light ramp (:root) and the original dark ramp (.dark). Because existing nb-gray-* utility classes resolve through those variables, most of the UI follows the active theme without per-component changes.

A working theme switcher (Light / Dark / System) is included, along with the supporting bits needed to make it feel right — system-preference resolution, themed loading skeletons, and a pre-paint guard to avoid a flash of the wrong theme on load.

Dark mode is intentionally unchanged — every fix adds a light-mode value (or a dark: override) without altering existing dark-mode appearance.

Changes

Theming core

  • Made nb-gray theme-aware via CSS variables (tailwind.config.ts + globals.css), preserving opacity modifiers (e.g. bg-nb-gray-900/80).
  • Defined a light ramp on :root and the original dark ramp under .dark.

Theme switching

  • Enabled the Light / Dark / System options in the theme toggle and surfaced it in the header.
  • System option resolves to the OS preference; loading skeletons follow the active theme.
  • Added a pre-hydration script to apply the stored theme before first paint and prevent a theme flash.

Shared-component contrast pass

Systematic light-mode contrast/visibility fixes in shared components, so the fix propagates everywhere they're used:

  • Badge and SquareIcon — all colored variants (red, green, sky, purple, yellow, netbird, …) made theme-aware (light pastel background + dark text) instead of dark-only boxes.
  • Callout, Notification (toast) — light text that was invisible on light surfaces now switches per theme; success/error toast icons render with proper contrast.
  • ToggleSwitch — enabled state is brand orange in light mode (was black).
  • Tabs, Modal (footer + content borders), Steps — stale nb-gray-100-as-light dividers (which invert to near-black) replaced with correct light tokens.
  • Paragraph, avatar/initials circles, the Settings Danger Zone, and empty-state list fillers — darkened/recolored for readable light-mode contrast.

Control Center (React Flow)

  • Drove the canvas colorMode from the active theme instead of a hardcoded "dark".
  • Made the canvas background, dot grid, and connection-edge strokes theme-aware (light gray canvas in light mode; dark unchanged).

Assets

  • Added a light-mode variant of the NetBird wordmark; the logo swaps by theme.

Issue ticket number and link

Documentation

Select exactly one:

  • I added/updated documentation
  • Documentation is not needed

Issue ticket number and link

#412 netbirdio/netbird#2918

Summary by CodeRabbit

  • New Features

    • Added system-aware theme selection with light, dark, and automatic (system) options.
    • Introduced improved dark mode support across all UI components.
  • Style

    • Updated comprehensive color palette for better visual consistency and readability.
    • Enhanced theme styling in modals, tables, buttons, and navigation components.
    • Improved visual appearance of control center and graph nodes based on current theme.
  • Bug Fixes

    • Prevented theme "flash" on page load with theme persistence.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

This PR introduces a client-side theme provider, migrates from next-themes to a custom ThemeProvider that reads localStorage and system preferences, applies CSS variable-backed colors, renders theme-aware components at runtime, and systematically updates component styling across the application to use the nb-gray-* token palette and light-mode-aware colors.

Changes

Theme System Consolidation and Palette Migration

Layer / File(s) Summary
Theme color contracts and global typography
src/app/globals.css, tailwind.config.ts
Global CSS defines --nb-gray-* CSS variable ramps for light and dark modes; Tailwind nb-gray colors switch to variable-backed rgb(var(...) / <alpha-value>) entries; base h1, h2, p typography use nb-gray text tokens directly.
Theme provider, bootstrap, and pre-paint setup
src/contexts/ThemeProvider.tsx, src/contexts/GlobalThemeProvider.tsx, src/layouts/AppLayout.tsx, package.json
New ThemeProvider exports Theme type, useTheme hook, and a provider component that reads stored theme from localStorage (fallback "dark"), detects system color preference via matchMedia, computes resolvedTheme, and persists changes. GlobalThemeProvider uses the new provider and wraps children with a skeleton wrapper that adjusts loading colors by theme. AppLayout injects an inline script to set the dark class before React hydration to prevent theme flash. The next-themes dependency is removed.
Theme selection UI and wiring
src/components/ui/DarkModeToggle.tsx, src/components/ui/UserDropdown.tsx
DarkModeToggle rewrites from a dropdown menu to a segmented button set that renders light, dark, and system options; each button reads the current theme via useTheme() and calls setTheme(value). UserDropdown integrates DarkModeToggle into the user menu via a separator and component render.
Runtime theme-aware rendering for visual surfaces
src/app/(dashboard)/control-center/page.tsx, src/modules/control-center/nodes/SelectGroupNode.tsx, src/modules/control-center/nodes/SelectPeerNode.tsx, src/modules/control-center/nodes/SelectUserNode.tsx, src/modules/control-center/edges/DirectionIn.tsx, src/modules/control-center/edges/SimpleConnection.tsx, src/modules/remote-access/ssh/Terminal.tsx, src/components/NetBirdLogo.tsx
ControlCenterView, graph nodes, and edges now call useTheme() to read resolvedTheme and set handle/stroke colors, background colors, and component styling conditionally. Terminal computes a derived terminalTheme from resolvedTheme and applies it during initialization and on theme change. NetBirdLogo imports a light-variant asset and switches between light and dark SVGs via CSS classes.
Shared component styling and token migration
src/components/Button.tsx, src/components/Badge.tsx, src/components/Code.tsx, src/components/DatePickerWithRange.tsx, src/components/HelpText.tsx, src/components/InlineLink.tsx, src/components/Input.tsx, src/components/Label.tsx, src/components/NetworkRouteSelector.tsx, src/components/Notification.tsx, src/components/PeerSelector.tsx, src/components/PinCodeInput.tsx, src/components/PortSelector.tsx, src/components/Separator.tsx, src/components/Tooltip.tsx, src/components/SquareIcon.tsx, src/components/Steps.tsx, src/components/Tabs.tsx, src/components/ToggleSwitch.tsx, src/components/FullTooltip.tsx, src/components/Paragraph.tsx, src/components/Callout.tsx, src/components/modal/Modal.tsx, src/components/ui/AnnouncementBanner.tsx, src/components/ui/GetStartedTest.tsx, src/components/ui/GroupBadgeWithEditPeers.tsx, src/components/ui/LoginExpiredBadge.tsx, src/components/ui/TruncatedText.tsx
Variant definitions, hover states, borders, and icon colors switch from neutral or white tokens to nb-gray equivalents with explicit light/dark class pairs where needed. Examples: buttons update danger, danger-outline, danger-text, default-outline variants; badges add dark-mode classes; tooltips use text-nb-gray-50 and border-nb-gray-930; modals switch to border-nb-gray-900; steps and tabs use bg-neutral-200 for light underlines.
Table and filter component styling alignment
src/components/table/DataTableFilter.tsx, src/components/table/DataTableHeader.tsx, src/components/table/DataTableMultiSelectPopup.tsx, src/components/table/DataTablePagination.tsx, src/components/table/DataTableRefreshButton.tsx, src/components/table/DataTableResetFilterButton.tsx, src/components/table/DataTableRowsPerPage.tsx, src/components/table/Table.tsx, src/components/table/TableFilters.tsx, src/components/table/filters/CheckboxListPicker.tsx, src/components/table/filters/RadioPicker.tsx, src/components/table/filters/StatusPicker.tsx
Pagination counts, filter buttons, selected-rows counters, header text, and row hover states converge on text-nb-gray-100 for emphasis and text-nb-gray-300 for secondary text. Picker options and checkboxes switch from text-white to text-nb-gray-100 and text-nb-gray-300 for consistency.
Page and selector surface palette updates
src/app/(dashboard)/group/page.tsx, src/app/(dashboard)/peer/page.tsx, src/app/invite/page.tsx, src/components/NetworkRouteSelector.tsx, src/components/PeerSelector.tsx, src/modules/peer/AddRouteDropdownButton.tsx, src/modules/peer/RemoteJobDropdownButton.tsx, src/modules/onboarding/networks/OnboardingTestResource.tsx, src/modules/onboarding/p2p/OnboardingTestP2P.tsx, src/modules/instance-setup/InstanceSetupWizard.tsx
Page headings, invite screens, peer/route selectors, peer action dropdowns, and onboarding text adopt nb-gray and light-mode-aware colors (e.g., text-nb-gray-100 for emphasis, text-nb-gray-900 for headings in light mode).
Feature cell, status, and context-specific palette updates
src/modules/access-control/AccessControlModal.tsx, src/modules/access-control/table/AccessControlDirectionCell.tsx, src/modules/activity/ActivityEntryRow.tsx, src/modules/activity/UsersDropdownSelector.tsx, src/modules/common-table-rows/ActiveInactiveRow.tsx, src/modules/common-table-rows/ExpirationDateRow.tsx, src/modules/common-table-rows/LastTimeRow.tsx, src/modules/control-center/user/ControlCenterCurrentUserBadge.tsx, src/modules/dns/nameservers/table/NameserverNameCell.tsx, src/modules/dns/zones/records/DNSRecordContentCell.tsx, src/modules/dns/zones/records/DNSRecordNameCell.tsx, src/modules/dns/zones/records/DNSRecordTimeToLiveCell.tsx, src/modules/groups/AssignPeerToGroupModal.tsx, src/modules/groups/table/GroupsNameCell.tsx, src/modules/networks/misc/NetworkInformationSquare.tsx, src/modules/networks/resources/NetworkResourceModal.tsx, src/modules/networks/resources/ResourceNameCell.tsx, src/modules/peers/PeerAddressCell.tsx, src/modules/peers/PeerConnectButton.tsx, src/modules/peers/PeerLastSeenCell.tsx, src/modules/peers/PeerMultiSelect.tsx, src/modules/peers/PeerNameCell.tsx, src/modules/peers/PeerOSCell.tsx, src/modules/peers/PeerVersionCell.tsx, src/modules/posture-checks/checks/tooltips/GeoLocationTooltip.tsx, src/modules/posture-checks/checks/tooltips/NetBirdVersionTooltip.tsx, src/modules/posture-checks/checks/tooltips/PeerNetworkRangeTooltip.tsx, src/modules/posture-checks/checks/tooltips/ProcessTooltip.tsx, src/modules/posture-checks/ui/PostureCheckCard.tsx, src/modules/posture-checks/ui/PostureCheckIcons.tsx, src/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation.tsx, src/modules/reverse-proxy/clusters/ClusterTypeIndicator.tsx, src/modules/reverse-proxy/clusters/ClustersFeaturesCell.tsx, src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx, src/modules/reverse-proxy/events/ReverseProxyEventsTimeCell.tsx, src/modules/reverse-proxy/events/ReverseProxyEventsUserCell.tsx, src/modules/reverse-proxy/table/ReverseProxyDestinationCell.tsx, src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx, src/modules/reverse-proxy/targets/ReverseProxyTargetSelector.tsx, src/modules/settings/ClientSettingsTab.tsx, src/modules/settings/DangerZoneTab.tsx, src/modules/settings/GroupsSettings.tsx, src/modules/users/UserInvitesTable.tsx, src/modules/users/table-cells/ServiceUserNameCell.tsx, src/modules/users/table-cells/UserNameCell.tsx, src/modules/users/table-cells/UserStatusCell.tsx
Tooltips, badges, cell text, avatar circles, status pills, icon fills, and warning callouts across all feature modules converge on text-nb-gray-300 for secondary text, text-nb-gray-100 for emphasis, text-neutral-700 dark:text-white for light-mode avatars, and explicit light/dark icon and background colors (e.g., direction cells, activity badges, DNS cells, peer cells, posture check status).
Repository local artifact ignore rules
.gitignore
Adds patterns to ignore local UI sandbox (/src/app/sandbox/), local config files (src/.local-config*), and mock data (src/utils/mockData.ts).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • netbirdio/dashboard#654: Overlaps on table filter picker styling updates (RadioPicker, StatusPicker, CheckboxListPicker) where both PRs adjust color tokens.
  • netbirdio/dashboard#547: Also modifies theme infrastructure in src/contexts/GlobalThemeProvider.tsx and theme wrapping approach.
  • netbirdio/dashboard#641: Touches src/modules/reverse-proxy/clusters/ClustersFeaturesCell.tsx where tooltip/label styling is adjusted alongside feature badge styling.

Suggested reviewers

  • mlsmaycon
  • heisbrot

🐰 A theme-colored dream so bright,
From dark to light, the system's right!
Gray tokens dance in CSS variables' keep,
While users choose their mode so deep. ✨🌙

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers the theme implementation strategy, shared component updates, Control Center changes, and assets. However, the template's required documentation section is incomplete. While documentation is marked as not needed, the template requires either a documentation PR link or a clear explanation of why it's not needed. Add a brief explanation after the checkbox.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding theme/light mode switching.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ui-accessibility

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.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

✅ Created PR with unit tests: #659

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

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

⚠️ Outside diff range comments (5)
src/layouts/Header.tsx (1)

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

hover:text-white was missed by the token migration and breaks in light mode.

The base color uses the theme-aware text-nb-gray-300, but the hover state is still hardcoded hover:text-white. With the inverted light ramp, hovering this nav-collapse button turns the icon white on a light background, making it effectively invisible. Use a theme-aware token instead.

🎨 Proposed fix
-          "h-10 w-10 hover:text-white flex items-center justify-center text-nb-gray-300 transition-all ml-2",
+          "h-10 w-10 hover:text-nb-gray-100 flex items-center justify-center text-nb-gray-300 transition-all ml-2",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/layouts/Header.tsx` at line 92, In the Header component's nav-collapse
button class string (the one containing "h-10 w-10 ... text-nb-gray-300"),
remove the hardcoded "hover:text-white" and replace it with the theme-aware
hover token used by our design system (e.g., "hover:text-nb-gray-50" or the
equivalent theme foreground token), so the hover icon color follows light/dark
ramps instead of becoming invisible in light mode.
src/app/(dashboard)/team/user/page.tsx (1)

177-200: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inline style overrides className text color change.

The className change to text-nb-gray-100 on line 179 has no effect because the inline style attribute (lines 181–193) sets an explicit color property that will override any class-based text color. For service users, color: "white" is applied; for regular users, a generated color is used. Inline styles always take precedence over CSS classes.

If the intent is to use the theme-aware nb-gray-100 token, remove the color property from the inline style object (keeping only the conditional logic if needed for other properties). Otherwise, the className text color is unreachable.

Proposed fix to remove conflicting inline color
               <div
                 className={
                   "w-10 h-10 rounded-full relative flex items-center justify-center text-nb-gray-100 uppercase text-md font-medium bg-nb-gray-900"
                 }
-                style={
-                  isServiceUser
-                    ? {
-                        color: "white",
-                      }
-                    : {
-                        color: user?.name
-                          ? generateColorFromString(
-                              user?.name || user?.id || "System User",
-                            )
-                          : "`#808080`",
-                      }
-                }
+                style={
+                  !isServiceUser && user?.name
+                    ? {
+                        color: generateColorFromString(
+                          user?.name || user?.id || "System User",
+                        ),
+                      }
+                    : undefined
+                }
               >

Note: If the generated color for regular users is intentional and should remain, then revert the text-nb-gray-100 className change since it serves no purpose.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/team/user/page.tsx around lines 177 - 200, The text
color set by the className "text-nb-gray-100" is being overridden by the inline
style color in the div that uses isServiceUser and generateColorFromString (the
block around the JSX div with className and style props); either remove the
color property from the inline style object so the class token applies (keep the
conditional shape but drop color), or if the generated per-user color is
intentional, revert removing the className so you don't claim nb-gray-100;
update the JSX around isServiceUser, generateColorFromString, and the style prop
accordingly.
src/components/Code.tsx (1)

43-44: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Incomplete theme token migration in background and border styles.

Lines 43-44 still use non-theme-aware classes (bg-gray-50, border-neutral-200) that won't adapt between light and dark modes. For consistent theme support, these should use the nb-gray palette like the text colors.

♻️ Proposed fix to complete the migration
         className={cn(
           "rounded-md text-sm  transition-all relative duration-300 border",
           dark
-            ? "bg-gray-50  border-neutral-200 dark:border-nb-gray-800 hover:dark:bg-nb-gray-900/80 dark:bg-nb-gray-930"
-            : "bg-gray-50  border-neutral-200 dark:border-nb-gray-700 hover:dark:bg-nb-gray-900/80 dark:bg-nb-gray-900",
+            ? "bg-nb-gray-50 border-nb-gray-200 dark:border-nb-gray-800 hover:dark:bg-nb-gray-900/80 dark:bg-nb-gray-930"
+            : "bg-nb-gray-50 border-nb-gray-200 dark:border-nb-gray-700 hover:dark:bg-nb-gray-900/80 dark:bg-nb-gray-900",
           dark
             ? "text-nb-gray-300 hover:text-nb-gray-300"
             : "text-nb-gray-200 hover:text-nb-gray-200",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Code.tsx` around lines 43 - 44, In the Code component in
src/components/Code.tsx update the class strings in the ternary that currently
use "bg-gray-50" and "border-neutral-200" to use the nb-gray palette instead
(e.g., replace with "bg-nb-gray-50" and "border-nb-gray-200" or your project's
nb-gray equivalents) so they match the existing dark-mode tokens (like
dark:border-nb-gray-800, dark:bg-nb-gray-930) and keep both branches consistent
with the nb-gray theme tokens.
src/modules/peers/PeerAddressCell.tsx (1)

57-57: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Complete the text token migration for the container.

The IP address text was correctly migrated to text-nb-gray-400 on line 68, but the parent container on line 57 still uses static dark:text-neutral-300 text-neutral-500. Complete the migration for consistency.

♻️ Proposed fix
-        <div className="flex flex-col gap-0 dark:text-neutral-300 text-neutral-500 font-light truncate">
+        <div className="flex flex-col gap-0 text-nb-gray-300 font-light truncate">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/peers/PeerAddressCell.tsx` at line 57, The parent container div
in PeerAddressCell.tsx still uses the old classes "dark:text-neutral-300
text-neutral-500"; update that container's className (the div with "flex
flex-col gap-0 ... truncate") to use the migrated token "text-nb-gray-400"
instead of those static neutral classes so it matches the IP text migration
while keeping the other classes (e.g., "flex flex-col gap-0 font-light
truncate") intact.
src/components/modal/Modal.tsx (1)

73-73: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Complete the border token migration for light mode.

ModalContent uses border-neutral-200 for light mode while dark mode correctly uses dark:border-nb-gray-900. For consistency with the theme-aware token system, light mode should also use an nb-gray-* token instead of the static neutral-200.

♻️ Proposed fix
-            "mx-auto relative top-0 z-[52] grid w-full focus:outline-0 border border-neutral-200 bg-white py-6 dark:shadow-lg shadow-sm duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1  data-[state=open]:slide-in-from-left-1 sm:rounded-lg md:w-full dark:border-nb-gray-900 dark:bg-nb-gray",
+            "mx-auto relative top-0 z-[52] grid w-full focus:outline-0 border border-nb-gray-100 bg-white py-6 dark:shadow-lg shadow-sm duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1  data-[state=open]:slide-in-from-left-1 sm:rounded-lg md:w-full dark:border-nb-gray-900 dark:bg-nb-gray",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/modal/Modal.tsx` at line 73, The Modal content is still using
the legacy border token "border-neutral-200"; update the class on the Modal
component (in Modal.tsx where the ModalContent grid wrapper string is defined)
to use the theme-aware token instead—replace "border-neutral-200" with the
appropriate nb-gray token (e.g., "border-nb-gray-200") so light mode uses
nb-gray like dark mode uses "dark:border-nb-gray-900".
🧹 Nitpick comments (3)
src/modules/dns/nameservers/table/NameserverNameCell.tsx (1)

14-14: 💤 Low value

Base text color left on neutral-* while hover migrated to nb-gray.

Only the hover state was switched to hover:text-nb-gray-100; the base still uses text-neutral-500/dark:text-neutral-300. It renders fine in both themes, but for token consistency with the rest of this migration consider moving the base color onto the nb-gray ramp too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/dns/nameservers/table/NameserverNameCell.tsx` at line 14, The
class string in the NameserverNameCell component still uses neutral tokens for
base text color while only hover uses nb-gray; update the class list in
NameserverNameCell (the JSX element containing the className value "flex
items-center gap-2 ...") to replace text-neutral-500 and dark:text-neutral-300
with the matching nb-gray tokens (e.g., text-nb-gray-300 / dark:text-nb-gray-300
or whatever nb-gray values your design system uses) so base and hover text
colors are on the same nb-gray ramp for consistency.
src/modules/control-center/nodes/SelectUserNode.tsx (1)

26-26: ⚡ Quick win

Use the CSS variable for the handle color in SelectUserNode (avoid resolvedTheme branching).

SelectUserNode currently branches on resolvedTheme to set the handle backgroundColor; driving it from --nb-gray-800 via rgb(var(--nb-gray-800)) removes the useTheme() dependency and keeps the color tied to the active theme.

♻️ Proposed change
-import { useTheme } from "next-themes";
 import * as React from "react";
 export const SelectUserNode = ({ data, id }: UserNodeProps) =&gt; {
-  const { resolvedTheme } = useTheme();
   const { data: users } = useFetchApi&lt;User[]&gt;("/users?service_user=false");
           height: 20,
           width: "1px",
           border: "none",
-          backgroundColor: resolvedTheme === "light" ? "`#dfe3e6`" : "`#3f444b`",
+          backgroundColor: "rgb(var(--nb-gray-800))",
           borderRadius: "0px 4px 4px 0px",
           right: -2,

Consider aligning the same approach in SelectGroupNode/SelectPeerNode if they use the same resolvedTheme/hex branching pattern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/control-center/nodes/SelectUserNode.tsx` at line 26,
SelectUserNode currently uses useTheme()/resolvedTheme to branch the handle
backgroundColor; remove the resolvedTheme dependency and set the handle style to
use the CSS variable (e.g., backgroundColor: "rgb(var(--nb-gray-800))") so the
handle color follows the active theme. Update the handle styling in the
SelectUserNode component (where handle backgroundColor is set) to use the CSS
variable string instead of conditional hex values and remove the useTheme
import/usage. Also scan SelectGroupNode and SelectPeerNode for the same
resolvedTheme/hex branching pattern and apply the same CSS-variable replacement
to keep all node handles theme-driven.
src/modules/remote-access/ssh/Terminal.tsx (1)

173-179: ⚡ Quick win

xterm.js runtime theme toggle repaints existing content—no refresh() needed.
In xterm.js 5.5.x, updating terminal.options.theme applies via internal renderer/color-manager updates and repaints the current buffer automatically, so an explicit terminal.refresh(…) call is unnecessary. Ensure terminalTheme is assigned as a new object reference on each toggle (avoid mutating the same theme object in place).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/modules/remote-access/ssh/Terminal.tsx` around lines 173 - 179, The
useEffect that assigns terminal.options.theme should simply set the new theme
object and not call terminal.refresh; update the code in the effect that reads
terminalInstanceRef.current?.terminal and assigns terminal.options.theme =
terminalTheme (keep as-is) and remove any explicit terminal.refresh calls;
ensure wherever you toggle/update terminalTheme you create a new object
reference (e.g., via spreading or constructing a new object) instead of mutating
the existing theme so xterm.js detects the change and repaints automatically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(dashboard)/peer/page.tsx:
- Line 257: The class string for the icon/button still uses legacy tokens
"dark:text-neutral-300 text-neutral-500"; update that class value (the string
starting with "flex h-8 w-8 items-center justify-center gap-2 ...") to use the
theme-aware nb tokens, e.g. replace "dark:text-neutral-300 text-neutral-500"
with "text-nb-gray-400" (keeping the existing "hover:text-nb-gray-100" and other
classes intact) so it matches the pattern used elsewhere like the example with
"text-nb-gray-400 hover:text-nb-gray-100".

In `@src/components/modal/Modal.tsx`:
- Line 138: The light-mode border token is still using border-neutral-200;
update SidebarModalContent (and ModalContent if present) to use the theme-aware
token (e.g., replace border-neutral-200 with border-nb-gray-200) in the class
string so light-mode styling matches the nb-gray tokens (also scan for any other
occurrences of border-neutral-200 in these components and replace them
accordingly).

In `@src/components/PinCodeInput.tsx`:
- Around line 111-112: In PinCodeInput.tsx there’s an incomplete token
migration: replace the old neutral token in the placeholder class under the
PinCodeInput component (the class string containing
"dark:placeholder:text-neutral-400/70") with the corresponding nb-gray token
(e.g., "dark:placeholder:text-nb-gray-400/70") so the placeholder uses the
nb-gray-* palette consistently with the other migrated classes.

In `@src/components/table/Table.tsx`:
- Line 131: TableHead currently mixes a static tailwind token by using
"text-neutral-500 dark:text-nb-gray-400 bg-nb-gray-900"; update the light-mode
text token to the theme-aware nb-gray family so both modes use nb-gray tokens.
Replace "text-neutral-500" with the appropriate nb-gray token (for example
"text-nb-gray-500") so the class becomes "text-nb-gray-500 dark:text-nb-gray-400
bg-nb-gray-900" in the TableHead rendering (locate the TableHead JSX / className
string in Table.tsx).

In `@src/modules/common-table-rows/LastTimeRow.tsx`:
- Line 29: The component LastTimeRow still mixes old neutral tokens with the new
nb tokens: update the root class string (currently containing
"dark:text-neutral-300 text-neutral-500 hover:text-nb-gray-100") to use the
corresponding nb tokens (e.g., replace dark:text-neutral-300 →
dark:text-nb-gray-200 and text-neutral-500 → text-nb-gray-300) and update the
tooltip text classes (the tooltip content lines currently using text-neutral-300
and text-neutral-200) to their nb equivalents (e.g., text-nb-gray-200 and
text-nb-gray-100) so all text color classes in the LastTimeRow component
consistently use the nb-* tokens.

In `@src/modules/dns/zones/records/DNSRecordTimeToLiveCell.tsx`:
- Line 14: In DNSRecordTimeToLiveCell, update the class string that currently
uses "dark:text-neutral-300 text-neutral-500" so it no longer uses any dark:
prefix or text-neutral-* tokens; replace them with the appropriate
"text-nb-gray-*" token (choosing a shade that matches the intended contrast for
the cell) while keeping the existing hover state "hover:text-nb-gray-100";
modify the class on the element that contains the string "flex items-center
whitespace-nowrap gap-2 dark:text-neutral-300 text-neutral-500
hover:text-nb-gray-100 transition-all py-2 px-3 rounded-md" to remove dark: and
neutral tokens and use the unified text-nb-gray-* token (e.g., text-nb-gray-200
or other selected shade).

In `@src/modules/groups/AssignPeerToGroupModal.tsx`:
- Line 234: In AssignPeerToGroupModal update the static text color classes in
the JSX location where the class string contains "flex items-center gap-2
dark:text-neutral-300 text-neutral-500 hover:text-nb-gray-100 ..." to use
theme-aware nb-gray tokens for both base and dark states (e.g., replace
dark:text-neutral-300 and text-neutral-500 with the appropriate nb-gray-*
equivalents) so all text states are consistent with the migrated hover token;
ensure you update the same class string used in the component's render/return so
hover, base, and dark text colors all use nb-gray tokens.

In `@src/modules/networks/misc/NetworkInformationSquare.tsx`:
- Around line 23-25: Replace the neutral tokens in the NetworkInformationSquare
component's class string so the base text color uses the nb-gray palette;
specifically, in the JSX where the class list is built (the fragment containing
"flex w-full items-center..." and the conditional using onClick), replace
"dark:text-neutral-300 text-neutral-500" with "text-nb-gray-500
dark:text-nb-gray-300" so the base text color matches the hover state tokens
(hover:text-nb-gray-100 / hover:bg-nb-gray-900/60) and remains consistent across
the NetworkInformationSquare component and its onClick variant.

In `@src/modules/networks/resources/ResourceNameCell.tsx`:
- Around line 38-39: The base text color in ResourceNameCell.tsx still uses the
old token "text-neutral-300"; update it to the theme token "text-nb-gray-300" to
match the hover state and complete the color token migration—locate the
className array/strings in ResourceNameCell (the JSX element where "flex
flex-col gap-0 text-neutral-300  font-light truncate" is defined) and replace
"text-neutral-300" with "text-nb-gray-300".

In `@src/modules/peers/PeerLastSeenCell.tsx`:
- Line 15: The class string in PeerLastSeenCell.tsx still uses static tokens
"dark:text-neutral-300 text-neutral-500" while hover was migrated; update the
JSX element's className (the string "flex items-center whitespace-nowrap gap-2
dark:text-neutral-300 text-neutral-500 hover:text-nb-gray-100 transition-all
py-2 px-3 rounded-md") to use theme-aware tokens instead (e.g., replace the
neutral tokens with matching nb-gray tokens such as "text-nb-gray-500
dark:text-nb-gray-300" while keeping hover:text-nb-gray-100), ensuring the
element's className reflects consistent theme token usage.

In `@src/modules/peers/PeerNameCell.tsx`:
- Around line 33-35: In PeerNameCell.tsx update the static color classes to the
theme-aware tokens: replace "dark:text-neutral-300 text-neutral-500" in the
className string with the corresponding nb tokens (e.g. "text-nb-gray-300
dark:text-nb-gray-300") so the base text color uses the same theme token family
as the migrated hover state ("hover:text-nb-gray-100"); ensure the change is
made in the class concatenation where linkToPeer is applied.

In `@src/modules/peers/PeerOSCell.tsx`:
- Line 29: Replace the hard-coded neutral color classes in the className string
inside PeerOSCell (the JSX line containing "flex items-center gap-2 ...") with
the theme-aware tokens to match the hover token already migrated; specifically
change "dark:text-neutral-300 text-neutral-500" to their nb-gray equivalents
(e.g., "dark:text-nb-gray-300 text-nb-gray-500") so the element uses consistent
theme tokens across base, dark, and hover states.

In `@src/modules/reverse-proxy/events/ReverseProxyEventsTimeCell.tsx`:
- Line 22: ReverseProxyEventsTimeCell uses old neutral tokens
("dark:text-neutral-300 text-neutral-500") alongside the migrated hover token
"hover:text-nb-gray-100", causing inconsistent theming; update the component's
className in ReverseProxyEventsTimeCell to replace the legacy neutral tokens
with the theme-aware token "text-nb-gray-300" so the base and hover states use
the same new token set and the component responds to light/dark themes
consistently.

---

Outside diff comments:
In `@src/app/`(dashboard)/team/user/page.tsx:
- Around line 177-200: The text color set by the className "text-nb-gray-100" is
being overridden by the inline style color in the div that uses isServiceUser
and generateColorFromString (the block around the JSX div with className and
style props); either remove the color property from the inline style object so
the class token applies (keep the conditional shape but drop color), or if the
generated per-user color is intentional, revert removing the className so you
don't claim nb-gray-100; update the JSX around isServiceUser,
generateColorFromString, and the style prop accordingly.

In `@src/components/Code.tsx`:
- Around line 43-44: In the Code component in src/components/Code.tsx update the
class strings in the ternary that currently use "bg-gray-50" and
"border-neutral-200" to use the nb-gray palette instead (e.g., replace with
"bg-nb-gray-50" and "border-nb-gray-200" or your project's nb-gray equivalents)
so they match the existing dark-mode tokens (like dark:border-nb-gray-800,
dark:bg-nb-gray-930) and keep both branches consistent with the nb-gray theme
tokens.

In `@src/components/modal/Modal.tsx`:
- Line 73: The Modal content is still using the legacy border token
"border-neutral-200"; update the class on the Modal component (in Modal.tsx
where the ModalContent grid wrapper string is defined) to use the theme-aware
token instead—replace "border-neutral-200" with the appropriate nb-gray token
(e.g., "border-nb-gray-200") so light mode uses nb-gray like dark mode uses
"dark:border-nb-gray-900".

In `@src/layouts/Header.tsx`:
- Line 92: In the Header component's nav-collapse button class string (the one
containing "h-10 w-10 ... text-nb-gray-300"), remove the hardcoded
"hover:text-white" and replace it with the theme-aware hover token used by our
design system (e.g., "hover:text-nb-gray-50" or the equivalent theme foreground
token), so the hover icon color follows light/dark ramps instead of becoming
invisible in light mode.

In `@src/modules/peers/PeerAddressCell.tsx`:
- Line 57: The parent container div in PeerAddressCell.tsx still uses the old
classes "dark:text-neutral-300 text-neutral-500"; update that container's
className (the div with "flex flex-col gap-0 ... truncate") to use the migrated
token "text-nb-gray-400" instead of those static neutral classes so it matches
the IP text migration while keeping the other classes (e.g., "flex flex-col
gap-0 font-light truncate") intact.

---

Nitpick comments:
In `@src/modules/control-center/nodes/SelectUserNode.tsx`:
- Line 26: SelectUserNode currently uses useTheme()/resolvedTheme to branch the
handle backgroundColor; remove the resolvedTheme dependency and set the handle
style to use the CSS variable (e.g., backgroundColor: "rgb(var(--nb-gray-800))")
so the handle color follows the active theme. Update the handle styling in the
SelectUserNode component (where handle backgroundColor is set) to use the CSS
variable string instead of conditional hex values and remove the useTheme
import/usage. Also scan SelectGroupNode and SelectPeerNode for the same
resolvedTheme/hex branching pattern and apply the same CSS-variable replacement
to keep all node handles theme-driven.

In `@src/modules/dns/nameservers/table/NameserverNameCell.tsx`:
- Line 14: The class string in the NameserverNameCell component still uses
neutral tokens for base text color while only hover uses nb-gray; update the
class list in NameserverNameCell (the JSX element containing the className value
"flex items-center gap-2 ...") to replace text-neutral-500 and
dark:text-neutral-300 with the matching nb-gray tokens (e.g., text-nb-gray-300 /
dark:text-nb-gray-300 or whatever nb-gray values your design system uses) so
base and hover text colors are on the same nb-gray ramp for consistency.

In `@src/modules/remote-access/ssh/Terminal.tsx`:
- Around line 173-179: The useEffect that assigns terminal.options.theme should
simply set the new theme object and not call terminal.refresh; update the code
in the effect that reads terminalInstanceRef.current?.terminal and assigns
terminal.options.theme = terminalTheme (keep as-is) and remove any explicit
terminal.refresh calls; ensure wherever you toggle/update terminalTheme you
create a new object reference (e.g., via spreading or constructing a new object)
instead of mutating the existing theme so xterm.js detects the change and
repaints automatically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e8a53ea7-d454-44ce-841f-1cb5b5dcf4ca

📥 Commits

Reviewing files that changed from the base of the PR and between 358b477 and a608188.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • src/assets/netbird-full-light.svg is excluded by !**/*.svg
📒 Files selected for processing (67)
  • src/app/(dashboard)/group/page.tsx
  • src/app/(dashboard)/peer/page.tsx
  • src/app/(dashboard)/team/user/page.tsx
  • src/app/globals.css
  • src/app/invite/page.tsx
  • src/components/Button.tsx
  • src/components/Code.tsx
  • src/components/DatePickerWithRange.tsx
  • src/components/FullTooltip.tsx
  • src/components/HelpText.tsx
  • src/components/InlineLink.tsx
  • src/components/Input.tsx
  • src/components/Label.tsx
  • src/components/NetBirdLogo.tsx
  • src/components/NetworkRouteSelector.tsx
  • src/components/Notification.tsx
  • src/components/PeerSelector.tsx
  • src/components/PinCodeInput.tsx
  • src/components/PortSelector.tsx
  • src/components/Separator.tsx
  • src/components/Tooltip.tsx
  • src/components/modal/Modal.tsx
  • src/components/table/DataTableFilter.tsx
  • src/components/table/DataTableMultiSelectPopup.tsx
  • src/components/table/DataTablePagination.tsx
  • src/components/table/DataTableRowsPerPage.tsx
  • src/components/table/Table.tsx
  • src/components/ui/DarkModeToggle.tsx
  • src/components/ui/HelpAndSupportButton.tsx
  • src/contexts/GlobalThemeProvider.tsx
  • src/layouts/AppLayout.tsx
  • src/layouts/Header.tsx
  • src/modules/access-control/AccessControlModal.tsx
  • src/modules/common-table-rows/LastTimeRow.tsx
  • src/modules/control-center/nodes/SelectGroupNode.tsx
  • src/modules/control-center/nodes/SelectPeerNode.tsx
  • src/modules/control-center/nodes/SelectUserNode.tsx
  • src/modules/dns/nameservers/table/NameserverNameCell.tsx
  • src/modules/dns/zones/records/DNSRecordTimeToLiveCell.tsx
  • src/modules/groups/AssignPeerToGroupModal.tsx
  • src/modules/instance-setup/InstanceSetupWizard.tsx
  • src/modules/networks/misc/NetworkInformationSquare.tsx
  • src/modules/networks/resources/ResourceNameCell.tsx
  • src/modules/onboarding/networks/OnboardingTestResource.tsx
  • src/modules/onboarding/p2p/OnboardingTestP2P.tsx
  • src/modules/peer/AddRouteDropdownButton.tsx
  • src/modules/peer/RemoteJobDropdownButton.tsx
  • src/modules/peers/PeerAddressCell.tsx
  • src/modules/peers/PeerConnectButton.tsx
  • src/modules/peers/PeerLastSeenCell.tsx
  • src/modules/peers/PeerMultiSelect.tsx
  • src/modules/peers/PeerNameCell.tsx
  • src/modules/peers/PeerOSCell.tsx
  • src/modules/peers/PeerVersionCell.tsx
  • src/modules/posture-checks/ui/PostureCheckIcons.tsx
  • src/modules/remote-access/rdp/RDPCertificateModal.tsx
  • src/modules/remote-access/ssh/Terminal.tsx
  • src/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation.tsx
  • src/modules/reverse-proxy/clusters/ClusterTypeIndicator.tsx
  • src/modules/reverse-proxy/clusters/ClustersFeaturesCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsTimeCell.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetModal.tsx
  • src/modules/reverse-proxy/targets/ReverseProxyTargetSelector.tsx
  • src/modules/settings/ClientSettingsTab.tsx
  • src/modules/users/table-cells/ServiceUserNameCell.tsx
  • src/modules/users/table-cells/UserStatusCell.tsx
  • tailwind.config.ts

Comment thread src/app/(dashboard)/peer/page.tsx Outdated
Comment thread src/components/modal/Modal.tsx Outdated
Comment thread src/components/PinCodeInput.tsx Outdated
Comment thread src/components/table/Table.tsx Outdated
Comment thread src/modules/common-table-rows/LastTimeRow.tsx Outdated
Comment thread src/modules/networks/resources/ResourceNameCell.tsx Outdated
Comment thread src/modules/peers/PeerLastSeenCell.tsx Outdated
Comment thread src/modules/peers/PeerNameCell.tsx Outdated
Comment thread src/modules/peers/PeerOSCell.tsx Outdated
Comment thread src/modules/reverse-proxy/events/ReverseProxyEventsTimeCell.tsx Outdated
@TechHutTV TechHutTV changed the title Light mode switching (experimental / draft) feat: Add Light Mode Switching (accessibility) Jun 3, 2026
@TechHutTV TechHutTV marked this pull request as ready for review June 5, 2026 20:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/table/filters/StatusPicker.tsx`:
- Around line 35-45: The buttons rendered in StatusPicker.tsx default to
type="submit" which can cause accidental form submissions; update the button
elements inside the options loop (the elements that call onClick ->
onChange(option.value) and close()) to include an explicit type="button"
attribute so clicking a status only triggers the picker logic (onChange/close)
and never submits a parent form.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba1620cf-0bdf-45d9-9cb4-8d3382918ae2

📥 Commits

Reviewing files that changed from the base of the PR and between a608188 and 1b79736.

📒 Files selected for processing (67)
  • .gitignore
  • src/app/(dashboard)/control-center/page.tsx
  • src/app/(dashboard)/group/page.tsx
  • src/app/(dashboard)/peer/page.tsx
  • src/components/Badge.tsx
  • src/components/Callout.tsx
  • src/components/FullTooltip.tsx
  • src/components/Notification.tsx
  • src/components/Paragraph.tsx
  • src/components/PinCodeInput.tsx
  • src/components/SquareIcon.tsx
  • src/components/Steps.tsx
  • src/components/Tabs.tsx
  • src/components/ToggleSwitch.tsx
  • src/components/modal/Modal.tsx
  • src/components/table/DataTableHeader.tsx
  • src/components/table/DataTableRefreshButton.tsx
  • src/components/table/DataTableResetFilterButton.tsx
  • src/components/table/Table.tsx
  • src/components/table/TableFilters.tsx
  • src/components/table/filters/CheckboxListPicker.tsx
  • src/components/table/filters/RadioPicker.tsx
  • src/components/table/filters/StatusPicker.tsx
  • src/components/ui/AnnouncementBanner.tsx
  • src/components/ui/GetStartedTest.tsx
  • src/components/ui/GroupBadgeWithEditPeers.tsx
  • src/components/ui/LoginExpiredBadge.tsx
  • src/components/ui/TruncatedText.tsx
  • src/contexts/GlobalThemeProvider.tsx
  • src/modules/access-control/table/AccessControlDirectionCell.tsx
  • src/modules/activity/ActivityEntryRow.tsx
  • src/modules/activity/UsersDropdownSelector.tsx
  • src/modules/common-table-rows/ActiveInactiveRow.tsx
  • src/modules/common-table-rows/ExpirationDateRow.tsx
  • src/modules/common-table-rows/LastTimeRow.tsx
  • src/modules/control-center/edges/DirectionIn.tsx
  • src/modules/control-center/edges/SimpleConnection.tsx
  • src/modules/control-center/nodes/SelectUserNode.tsx
  • src/modules/control-center/user/ControlCenterCurrentUserBadge.tsx
  • src/modules/dns/nameservers/table/NameserverNameCell.tsx
  • src/modules/dns/zones/records/DNSRecordContentCell.tsx
  • src/modules/dns/zones/records/DNSRecordNameCell.tsx
  • src/modules/dns/zones/records/DNSRecordTimeToLiveCell.tsx
  • src/modules/groups/AssignPeerToGroupModal.tsx
  • src/modules/groups/table/GroupsNameCell.tsx
  • src/modules/networks/misc/NetworkInformationSquare.tsx
  • src/modules/networks/resources/NetworkResourceModal.tsx
  • src/modules/networks/resources/ResourceNameCell.tsx
  • src/modules/peers/PeerAddressCell.tsx
  • src/modules/peers/PeerLastSeenCell.tsx
  • src/modules/peers/PeerNameCell.tsx
  • src/modules/peers/PeerOSCell.tsx
  • src/modules/peers/PeerVersionCell.tsx
  • src/modules/posture-checks/checks/tooltips/GeoLocationTooltip.tsx
  • src/modules/posture-checks/checks/tooltips/NetBirdVersionTooltip.tsx
  • src/modules/posture-checks/checks/tooltips/PeerNetworkRangeTooltip.tsx
  • src/modules/posture-checks/checks/tooltips/ProcessTooltip.tsx
  • src/modules/posture-checks/ui/PostureCheckCard.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsTimeCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsUserCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyDestinationCell.tsx
  • src/modules/settings/DangerZoneTab.tsx
  • src/modules/settings/GroupsSettings.tsx
  • src/modules/users/UserInvitesTable.tsx
  • src/modules/users/table-cells/UserNameCell.tsx
  • src/modules/users/table-cells/UserStatusCell.tsx
✅ Files skipped from review due to trivial changes (43)
  • src/components/Paragraph.tsx
  • src/modules/dns/zones/records/DNSRecordContentCell.tsx
  • src/components/ui/LoginExpiredBadge.tsx
  • src/modules/dns/zones/records/DNSRecordNameCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx
  • src/components/table/DataTableResetFilterButton.tsx
  • src/components/ui/TruncatedText.tsx
  • src/modules/posture-checks/checks/tooltips/PeerNetworkRangeTooltip.tsx
  • src/modules/reverse-proxy/table/ReverseProxyDestinationCell.tsx
  • src/components/table/DataTableHeader.tsx
  • src/modules/dns/nameservers/table/NameserverNameCell.tsx
  • src/modules/groups/table/GroupsNameCell.tsx
  • src/modules/common-table-rows/ExpirationDateRow.tsx
  • src/components/ui/GetStartedTest.tsx
  • src/app/(dashboard)/group/page.tsx
  • src/components/table/TableFilters.tsx
  • src/modules/common-table-rows/ActiveInactiveRow.tsx
  • src/components/table/filters/RadioPicker.tsx
  • src/modules/posture-checks/checks/tooltips/GeoLocationTooltip.tsx
  • src/components/ToggleSwitch.tsx
  • src/modules/activity/UsersDropdownSelector.tsx
  • src/app/(dashboard)/peer/page.tsx
  • src/modules/peers/PeerLastSeenCell.tsx
  • src/modules/users/table-cells/UserStatusCell.tsx
  • src/components/table/filters/CheckboxListPicker.tsx
  • src/modules/posture-checks/checks/tooltips/NetBirdVersionTooltip.tsx
  • src/modules/posture-checks/checks/tooltips/ProcessTooltip.tsx
  • src/components/table/DataTableRefreshButton.tsx
  • src/modules/control-center/user/ControlCenterCurrentUserBadge.tsx
  • src/modules/users/UserInvitesTable.tsx
  • src/modules/settings/DangerZoneTab.tsx
  • src/modules/networks/resources/NetworkResourceModal.tsx
  • src/modules/peers/PeerVersionCell.tsx
  • src/components/PinCodeInput.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsUserCell.tsx
  • src/components/table/Table.tsx
  • src/modules/peers/PeerNameCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsTimeCell.tsx
  • src/components/Tabs.tsx
  • src/components/Notification.tsx
  • src/components/Steps.tsx
  • src/modules/peers/PeerOSCell.tsx
  • src/components/modal/Modal.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/modules/groups/AssignPeerToGroupModal.tsx
  • src/modules/networks/misc/NetworkInformationSquare.tsx
  • src/components/FullTooltip.tsx
  • src/modules/dns/zones/records/DNSRecordTimeToLiveCell.tsx
  • src/modules/peers/PeerAddressCell.tsx
  • src/modules/control-center/nodes/SelectUserNode.tsx
  • src/contexts/GlobalThemeProvider.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/table/filters/StatusPicker.tsx`:
- Around line 35-45: The buttons rendered in StatusPicker.tsx default to
type="submit" which can cause accidental form submissions; update the button
elements inside the options loop (the elements that call onClick ->
onChange(option.value) and close()) to include an explicit type="button"
attribute so clicking a status only triggers the picker logic (onChange/close)
and never submits a parent form.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba1620cf-0bdf-45d9-9cb4-8d3382918ae2

📥 Commits

Reviewing files that changed from the base of the PR and between a608188 and 1b79736.

📒 Files selected for processing (67)
  • .gitignore
  • src/app/(dashboard)/control-center/page.tsx
  • src/app/(dashboard)/group/page.tsx
  • src/app/(dashboard)/peer/page.tsx
  • src/components/Badge.tsx
  • src/components/Callout.tsx
  • src/components/FullTooltip.tsx
  • src/components/Notification.tsx
  • src/components/Paragraph.tsx
  • src/components/PinCodeInput.tsx
  • src/components/SquareIcon.tsx
  • src/components/Steps.tsx
  • src/components/Tabs.tsx
  • src/components/ToggleSwitch.tsx
  • src/components/modal/Modal.tsx
  • src/components/table/DataTableHeader.tsx
  • src/components/table/DataTableRefreshButton.tsx
  • src/components/table/DataTableResetFilterButton.tsx
  • src/components/table/Table.tsx
  • src/components/table/TableFilters.tsx
  • src/components/table/filters/CheckboxListPicker.tsx
  • src/components/table/filters/RadioPicker.tsx
  • src/components/table/filters/StatusPicker.tsx
  • src/components/ui/AnnouncementBanner.tsx
  • src/components/ui/GetStartedTest.tsx
  • src/components/ui/GroupBadgeWithEditPeers.tsx
  • src/components/ui/LoginExpiredBadge.tsx
  • src/components/ui/TruncatedText.tsx
  • src/contexts/GlobalThemeProvider.tsx
  • src/modules/access-control/table/AccessControlDirectionCell.tsx
  • src/modules/activity/ActivityEntryRow.tsx
  • src/modules/activity/UsersDropdownSelector.tsx
  • src/modules/common-table-rows/ActiveInactiveRow.tsx
  • src/modules/common-table-rows/ExpirationDateRow.tsx
  • src/modules/common-table-rows/LastTimeRow.tsx
  • src/modules/control-center/edges/DirectionIn.tsx
  • src/modules/control-center/edges/SimpleConnection.tsx
  • src/modules/control-center/nodes/SelectUserNode.tsx
  • src/modules/control-center/user/ControlCenterCurrentUserBadge.tsx
  • src/modules/dns/nameservers/table/NameserverNameCell.tsx
  • src/modules/dns/zones/records/DNSRecordContentCell.tsx
  • src/modules/dns/zones/records/DNSRecordNameCell.tsx
  • src/modules/dns/zones/records/DNSRecordTimeToLiveCell.tsx
  • src/modules/groups/AssignPeerToGroupModal.tsx
  • src/modules/groups/table/GroupsNameCell.tsx
  • src/modules/networks/misc/NetworkInformationSquare.tsx
  • src/modules/networks/resources/NetworkResourceModal.tsx
  • src/modules/networks/resources/ResourceNameCell.tsx
  • src/modules/peers/PeerAddressCell.tsx
  • src/modules/peers/PeerLastSeenCell.tsx
  • src/modules/peers/PeerNameCell.tsx
  • src/modules/peers/PeerOSCell.tsx
  • src/modules/peers/PeerVersionCell.tsx
  • src/modules/posture-checks/checks/tooltips/GeoLocationTooltip.tsx
  • src/modules/posture-checks/checks/tooltips/NetBirdVersionTooltip.tsx
  • src/modules/posture-checks/checks/tooltips/PeerNetworkRangeTooltip.tsx
  • src/modules/posture-checks/checks/tooltips/ProcessTooltip.tsx
  • src/modules/posture-checks/ui/PostureCheckCard.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsTimeCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsUserCell.tsx
  • src/modules/reverse-proxy/table/ReverseProxyDestinationCell.tsx
  • src/modules/settings/DangerZoneTab.tsx
  • src/modules/settings/GroupsSettings.tsx
  • src/modules/users/UserInvitesTable.tsx
  • src/modules/users/table-cells/UserNameCell.tsx
  • src/modules/users/table-cells/UserStatusCell.tsx
✅ Files skipped from review due to trivial changes (43)
  • src/components/Paragraph.tsx
  • src/modules/dns/zones/records/DNSRecordContentCell.tsx
  • src/components/ui/LoginExpiredBadge.tsx
  • src/modules/dns/zones/records/DNSRecordNameCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsRequestCell.tsx
  • src/components/table/DataTableResetFilterButton.tsx
  • src/components/ui/TruncatedText.tsx
  • src/modules/posture-checks/checks/tooltips/PeerNetworkRangeTooltip.tsx
  • src/modules/reverse-proxy/table/ReverseProxyDestinationCell.tsx
  • src/components/table/DataTableHeader.tsx
  • src/modules/dns/nameservers/table/NameserverNameCell.tsx
  • src/modules/groups/table/GroupsNameCell.tsx
  • src/modules/common-table-rows/ExpirationDateRow.tsx
  • src/components/ui/GetStartedTest.tsx
  • src/app/(dashboard)/group/page.tsx
  • src/components/table/TableFilters.tsx
  • src/modules/common-table-rows/ActiveInactiveRow.tsx
  • src/components/table/filters/RadioPicker.tsx
  • src/modules/posture-checks/checks/tooltips/GeoLocationTooltip.tsx
  • src/components/ToggleSwitch.tsx
  • src/modules/activity/UsersDropdownSelector.tsx
  • src/app/(dashboard)/peer/page.tsx
  • src/modules/peers/PeerLastSeenCell.tsx
  • src/modules/users/table-cells/UserStatusCell.tsx
  • src/components/table/filters/CheckboxListPicker.tsx
  • src/modules/posture-checks/checks/tooltips/NetBirdVersionTooltip.tsx
  • src/modules/posture-checks/checks/tooltips/ProcessTooltip.tsx
  • src/components/table/DataTableRefreshButton.tsx
  • src/modules/control-center/user/ControlCenterCurrentUserBadge.tsx
  • src/modules/users/UserInvitesTable.tsx
  • src/modules/settings/DangerZoneTab.tsx
  • src/modules/networks/resources/NetworkResourceModal.tsx
  • src/modules/peers/PeerVersionCell.tsx
  • src/components/PinCodeInput.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsUserCell.tsx
  • src/components/table/Table.tsx
  • src/modules/peers/PeerNameCell.tsx
  • src/modules/reverse-proxy/events/ReverseProxyEventsTimeCell.tsx
  • src/components/Tabs.tsx
  • src/components/Notification.tsx
  • src/components/Steps.tsx
  • src/modules/peers/PeerOSCell.tsx
  • src/components/modal/Modal.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/modules/groups/AssignPeerToGroupModal.tsx
  • src/modules/networks/misc/NetworkInformationSquare.tsx
  • src/components/FullTooltip.tsx
  • src/modules/dns/zones/records/DNSRecordTimeToLiveCell.tsx
  • src/modules/peers/PeerAddressCell.tsx
  • src/modules/control-center/nodes/SelectUserNode.tsx
  • src/contexts/GlobalThemeProvider.tsx
🛑 Comments failed to post (1)
src/components/table/filters/StatusPicker.tsx (1)

35-45: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Set explicit button type to avoid accidental form submissions.

<button> defaults to type="submit". If this picker is rendered inside a form context, selecting a status can submit the form unexpectedly.

Proposed fix
           <button
             key={option.label}
+            type="button"
             className={cn(
               "flex items-center gap-2.5 px-2 py-1.5 rounded text-sm transition-colors",
               "text-nb-gray-300 hover:bg-nb-gray-900 hover:text-nb-gray-100",
               selected && "text-nb-gray-100",
             )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/table/filters/StatusPicker.tsx` around lines 35 - 45, The
buttons rendered in StatusPicker.tsx default to type="submit" which can cause
accidental form submissions; update the button elements inside the options loop
(the elements that call onClick -> onChange(option.value) and close()) to
include an explicit type="button" attribute so clicking a status only triggers
the picker logic (onChange/close) and never submits a parent form.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/contexts/GlobalThemeProvider.tsx (1)

25-26: ⚠️ Potential issue | 🟠 Major

Use --nb-gray-* values for skeleton colors (two match exactly, two drift).

In src/contexts/GlobalThemeProvider.tsx, light highlightColor (#f8fafb = 248 250 251) matches --nb-gray-940, and dark baseColor (#25282d = 37 40 45) matches --nb-gray-920. Light baseColor (#edeff1) and dark highlightColor (#33373e) don’t match any --nb-gray-* value exactly (they’re slightly off from the nearest stops). Derive them from CSS vars to prevent theme drift, e.g.:

baseColor={isLight ? "rgb(var(--nb-gray-900))" : "rgb(var(--nb-gray-920))"}
highlightColor={isLight ? "rgb(var(--nb-gray-940))" : "rgb(var(--nb-gray-850))"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/GlobalThemeProvider.tsx` around lines 25 - 26, Replace the
hardcoded hex skeleton colors in GlobalThemeProvider (the
baseColor/highlightColor ternaries using isLight) with CSS variable references
so they exactly match the design tokens and avoid drift; set light baseColor to
rgb(var(--nb-gray-900)), light highlightColor to rgb(var(--nb-gray-940)), dark
baseColor to rgb(var(--nb-gray-920)) and dark highlightColor to
rgb(var(--nb-gray-850)) so the baseColor/highlightColor branches use those
rgb(var(--nb-gray-...)) vars instead of the current hex literals.
🧹 Nitpick comments (2)
src/contexts/GlobalThemeProvider.tsx (1)

14-18: 💤 Low value

Clarify or remove misleading comment.

The comment states "Defaults to the dark palette before mount to match the dark default theme," but the component always uses the current resolvedTheme value from context. There is no explicit "before mount" default in this component—the palette is determined by whatever resolvedTheme returns on first render (which comes from getStoredTheme() or system preference).

📝 Suggested comment revision
 /**
  * Wraps the skeleton loader theme so its colors follow the active theme.
  * Uses `resolvedTheme` so the "system" option resolves to the real OS value.
- * Defaults to the dark palette before mount to match the dark default theme.
  */

Or revise to clarify:

 /**
  * Wraps the skeleton loader theme so its colors follow the active theme.
  * Uses `resolvedTheme` so the "system" option resolves to the real OS value.
+ * On first render, falls back to the stored theme or "dark" default (via ThemeProvider).
  */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/GlobalThemeProvider.tsx` around lines 14 - 18, Update the top
comment in GlobalThemeProvider.tsx to remove the inaccurate "before mount" claim
and state that the skeleton loader palette follows the current resolvedTheme on
first render; mention that resolvedTheme (from useTheme/getStoredTheme or system
preference) determines the initial palette rather than an explicit dark default.
Reference the component name GlobalThemeProvider and the
resolvedTheme/getStoredTheme sources so the comment accurately reflects
behavior.
src/contexts/ThemeProvider.tsx (1)

98-98: ⚡ Quick win

Consider guarding useTheme against usage outside provider.

The hook returns the default context value (with a no-op setTheme) when called outside ThemeProvider. This silently fails instead of surfacing the integration bug. Consider throwing an error in development or logging a warning.

♻️ Proposed guard for development
-export const useTheme = () => React.useContext(ThemeContext);
+export const useTheme = () => {
+  const context = React.useContext(ThemeContext);
+  if (process.env.NODE_ENV !== "production" && context.setTheme === ThemeContext._currentValue.setTheme) {
+    console.warn("useTheme() called outside ThemeProvider. Theme changes will not persist.");
+  }
+  return context;
+};

Alternatively, throw an error to catch bugs earlier:

-export const useTheme = () => React.useContext(ThemeContext);
+export const useTheme = () => {
+  const context = React.useContext(ThemeContext);
+  if (context === ThemeContext._currentValue) {
+    throw new Error("useTheme must be used within ThemeProvider");
+  }
+  return context;
+};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/ThemeProvider.tsx` at line 98, The useTheme hook returns a safe
default when used outside ThemeProvider, which hides integration bugs—modify
useTheme to guard against misuse by reading const ctx =
React.useContext(ThemeContext) and, in development (e.g. process.env.NODE_ENV
!== 'production'), throw a clear error (or at minimum console.warn) if ctx is
missing or equals the default sentinel (inspect setTheme/no-op) so callers see
an immediate failure; reference useTheme, ThemeContext, ThemeProvider and the
default setTheme no-op when implementing the guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/contexts/ThemeProvider.tsx`:
- Around line 42-55: The withTransitionsDisabled function risks leaking the
temporary <style> if apply() throws; wrap the call to apply() and the subsequent
getComputedStyle() in a try/finally so the style element created in
withTransitionsDisabled is always removed from document.head (use
document.head.contains(style) or guard removal) even when an exception occurs,
preserving the existing behavior of scheduling removal with setTimeout but
ensuring the immediate cleanup path in finally handles errors safely.

---

Outside diff comments:
In `@src/contexts/GlobalThemeProvider.tsx`:
- Around line 25-26: Replace the hardcoded hex skeleton colors in
GlobalThemeProvider (the baseColor/highlightColor ternaries using isLight) with
CSS variable references so they exactly match the design tokens and avoid drift;
set light baseColor to rgb(var(--nb-gray-900)), light highlightColor to
rgb(var(--nb-gray-940)), dark baseColor to rgb(var(--nb-gray-920)) and dark
highlightColor to rgb(var(--nb-gray-850)) so the baseColor/highlightColor
branches use those rgb(var(--nb-gray-...)) vars instead of the current hex
literals.

---

Nitpick comments:
In `@src/contexts/GlobalThemeProvider.tsx`:
- Around line 14-18: Update the top comment in GlobalThemeProvider.tsx to remove
the inaccurate "before mount" claim and state that the skeleton loader palette
follows the current resolvedTheme on first render; mention that resolvedTheme
(from useTheme/getStoredTheme or system preference) determines the initial
palette rather than an explicit dark default. Reference the component name
GlobalThemeProvider and the resolvedTheme/getStoredTheme sources so the comment
accurately reflects behavior.

In `@src/contexts/ThemeProvider.tsx`:
- Line 98: The useTheme hook returns a safe default when used outside
ThemeProvider, which hides integration bugs—modify useTheme to guard against
misuse by reading const ctx = React.useContext(ThemeContext) and, in development
(e.g. process.env.NODE_ENV !== 'production'), throw a clear error (or at minimum
console.warn) if ctx is missing or equals the default sentinel (inspect
setTheme/no-op) so callers see an immediate failure; reference useTheme,
ThemeContext, ThemeProvider and the default setTheme no-op when implementing the
guard.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 263b2c6b-9257-41b1-91eb-b62f8c1d2aa0

📥 Commits

Reviewing files that changed from the base of the PR and between 1b79736 and 672a135.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • package.json
  • src/app/(dashboard)/control-center/page.tsx
  • src/components/ui/DarkModeToggle.tsx
  • src/contexts/GlobalThemeProvider.tsx
  • src/contexts/ThemeProvider.tsx
  • src/layouts/AppLayout.tsx
  • src/modules/control-center/edges/DirectionIn.tsx
  • src/modules/control-center/edges/SimpleConnection.tsx
  • src/modules/control-center/nodes/SelectGroupNode.tsx
  • src/modules/control-center/nodes/SelectPeerNode.tsx
  • src/modules/control-center/nodes/SelectUserNode.tsx
  • src/modules/remote-access/ssh/Terminal.tsx
💤 Files with no reviewable changes (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/modules/control-center/edges/DirectionIn.tsx
  • src/modules/control-center/nodes/SelectGroupNode.tsx
  • src/app/(dashboard)/control-center/page.tsx
  • src/modules/control-center/nodes/SelectPeerNode.tsx
  • src/modules/control-center/nodes/SelectUserNode.tsx
  • src/layouts/AppLayout.tsx
  • src/modules/control-center/edges/SimpleConnection.tsx
  • src/components/ui/DarkModeToggle.tsx
  • src/modules/remote-access/ssh/Terminal.tsx

Comment thread src/contexts/ThemeProvider.tsx
@TechHutTV TechHutTV changed the title feat: Add Light Mode Switching (accessibility) feat: Add Light Mode Switching Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant