Skip to content

feat(ui): implement dynamic theme engine with custom tweakcn presets - #20

Merged
pramodnarayana merged 5 commits into
developmentfrom
feat/admin-crud-tenants
Jan 19, 2026
Merged

pramodnarayana merged 5 commits into
developmentfrom
feat/admin-crud-tenants

Conversation

@pramodnarayana

@pramodnarayana pramodnarayana commented Jan 19, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Theme switcher added to the admin UI with multiple curated themes; selection persists and applies across light/dark modes.
  • UI Updates

    • Global re-theme with updated semantic color tokens for consistent light/dark visuals and improved contrast.
    • Navigation, avatars, badges, status indicators and layout styling updated to use the new semantic variants.
  • Tests

    • Layout tests updated to run within the theming context.

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

@coderabbitai

coderabbitai Bot commented Jan 19, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a theming system: typed ThemeContext, ThemeProvider (with persistence and DOM sync), theme registry, useTheme hook, ThemeSwitcher UI, ThemeProvider wired into app bootstrap, and updates to global CSS tokens and layout styling to use semantic theme variables.

Changes

Cohort / File(s) Summary
Theme infrastructure
apps/web/src/lib/theme/ThemeContext.ts, apps/web/src/lib/theme/ThemeProvider.tsx, apps/web/src/lib/theme/themes.ts, apps/web/src/lib/theme/useTheme.ts
New typed ThemeProviderContext and ThemeProvider component (localStorage persistence, applies cssVars, MutationObserver to resync), a large static themes registry, and useTheme hook.
ThemeSwitcher component
apps/web/src/components/ThemeSwitcher.tsx
New exported React component that consumes useTheme, renders an icon + select of availableThemes, and calls setTheme on change.
App bootstrap
apps/web/src/main.tsx
App now wrapped with <ThemeProvider defaultTheme="violet-bloom" storageKey="vite-ui-theme-preset">.
Global styles
apps/web/src/index.css
Re-theming of semantic CSS variables for light/dark (primary, ring, card, popover, border, input, secondary, muted, accent, destructive, chart tokens).
Layout integrations & styling
apps/web/src/layouts/AdminLayout.tsx, apps/web/src/layouts/TenantLayout.tsx
Injected ThemeSwitcher into AdminLayout; replaced many Tailwind color classes with semantic CSS token classes (foreground, muted-foreground, primary, destructive, border, etc.).
Tests
apps/web/src/layouts/AdminLayout.spec.tsx
Tests updated to wrap AdminLayout renders with ThemeProvider (defaultTheme="violet-bloom", storageKey="test-theme") and clear localStorage in setup.
Status badges
apps/web/src/modules/tenants/TenantList.tsx
Tenant status badge variants changed from hardcoded color classes to semantic token classes (primary/muted/destructive variants).

Sequence Diagram(s)

sequenceDiagram
    participant App as App (main.tsx)
    participant TP as ThemeProvider
    participant Store as localStorage
    participant Registry as Themes Registry
    participant Root as DocumentRoot
    participant Switcher as ThemeSwitcher
    participant User as User

    App->>TP: mount (defaultTheme)
    TP->>Store: read(storageKey)
    alt stored theme found
        Store-->>TP: storedTheme
    else
        TP-->>TP: use defaultTheme
    end
    TP->>Registry: lookup cssVars for theme
    Registry-->>TP: cssVars (light/dark)
    TP->>Root: apply CSS variables
    TP->>Root: observe class mutations (MutationObserver)
    TP-->>App: provide context {theme, setTheme, availableThemes}

    App->>Switcher: render (useTheme -> context)
    Switcher->>Switcher: display availableThemes

    User->>Switcher: select newTheme
    Switcher->>TP: setTheme(newTheme)
    TP->>Registry: fetch cssVars for newTheme
    Registry-->>TP: return cssVars
    TP->>Root: update CSS variables
    TP->>Store: persist(newTheme)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🥕 I twitch my whiskers, pick a tone,
I swap the light, I flip the chrome,
From violet bloom to midnight's glow,
I hop, I paint—your themes now show,
A rabbit's brush that changes home.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a dynamic theme engine with custom color presets across the application.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

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

🤖 Fix all issues with AI agents
In `@apps/web/src/lib/theme/ThemeContext.ts`:
- Around line 10-14: initialState defines setTheme as a stub that returns null
while ThemeProviderState expects () => void; change the stub to be a
void-returning function (e.g., () => { } or () => undefined) so its return type
matches the declared signature; update the initialState object (symbol:
initialState) and ensure setTheme's implementation aligns with the
ThemeProviderState type to remove the inconsistency.

In `@apps/web/src/lib/theme/ThemeProvider.tsx`:
- Around line 21-37: The theme resolution and CSS variable application
duplicated in ThemeProvider's two useEffect hooks should be extracted into a
helper: create a function (e.g., resolveAndApplyThemeCssVars or
applyThemeCssVars) that accepts (theme, defaultTheme, storageKey,
root=document.documentElement) and performs the logic currently repeated —
resolve currentTheme via themes[theme] || themes[defaultTheme] ||
Object.values(themes)[0], determine isDark by checking
root.classList.contains("dark"), select cssVars (currentTheme.cssVars.dark or
.light), apply Object.entries(cssVars).forEach(...) to set CSS properties on
root, and persist localStorage.setItem(storageKey, theme); then call this helper
from both useEffect hooks in place of the duplicated blocks.
- Around line 17-19: The useState initializer in ThemeProvider that calls
localStorage.getItem(storageKey) and the code that calls localStorage.setItem
when updating theme should be wrapped in defensive try-catch logic: in the
initializer for theme (the useState callback referencing storageKey and
defaultTheme) catch any exception from localStorage.getItem and return
defaultTheme on error, and in the theme update path (where setTheme and
localStorage.setItem are used) guard the localStorage.setItem call with
try-catch (or a feature check) so failures are swallowed/logged and do not break
state updates; reference the ThemeProvider component, the
storageKey/defaultTheme symbols, and the setTheme/localStorage.setItem usage to
locate where to apply these changes.

In `@apps/web/src/lib/theme/themes.ts`:
- Around line 10-551: The dark variants across themes are missing the "--radius"
token, causing inconsistent corner rounding (e.g., "pastel-dreams" light sets
"--radius": "0.75rem" but its dark map lacks it). For each theme in the exported
themes object (look for cssVars -> dark in "modern-minimal", "clean-slate",
"bold-tech", "cappuccino", "violet-bloom", "darkmatter", "midnight-bloom",
"northern-lights", "twitter", "pastel-dreams", "cosmic-night", etc.), add a
"--radius" entry in the dark map matching the intended radius (or unify by
moving "--radius" into a shared place applied to both light and dark) so both
light and dark maps define the same radius token.

In `@apps/web/src/lib/theme/useTheme.ts`:
- Around line 4-10: The hook useTheme currently checks for undefined but
ThemeProviderContext is created with initialState so the check never fires;
update the context/provider pattern: change ThemeProviderContext's default to
undefined and its type to possibly undefined in ThemeContext.ts (so
useContext(ThemeProviderContext) can return undefined), then keep the existing
undefined check in useTheme (throwing when context is undefined), or
alternatively leave the default as initialState and modify useTheme to detect a
sentinel value from initialState (e.g., a specific flag like isInitialized) and
throw when that sentinel indicates no provider; reference ThemeProviderContext,
initialState, useTheme, and ThemeContext.ts when making the change.

Comment thread apps/web/src/lib/theme/ThemeContext.ts Outdated
Comment on lines +10 to +14
export const initialState: ThemeProviderState = {
theme: "bold-tech",
setTheme: () => null,
availableThemes: [],
};

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.

🧹 Nitpick | 🔵 Trivial

Minor: setTheme returns null but type declares void.

The setTheme stub returns null, but the type signature declares () => void. While this works at runtime, it's slightly inconsistent.

Suggested fix
 export const initialState: ThemeProviderState = {
     theme: "bold-tech",
-    setTheme: () => null,
+    setTheme: () => {},
     availableThemes: [],
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const initialState: ThemeProviderState = {
theme: "bold-tech",
setTheme: () => null,
availableThemes: [],
};
export const initialState: ThemeProviderState = {
theme: "bold-tech",
setTheme: () => {},
availableThemes: [],
};
🤖 Prompt for AI Agents
In `@apps/web/src/lib/theme/ThemeContext.ts` around lines 10 - 14, initialState
defines setTheme as a stub that returns null while ThemeProviderState expects ()
=> void; change the stub to be a void-returning function (e.g., () => { } or ()
=> undefined) so its return type matches the declared signature; update the
initialState object (symbol: initialState) and ensure setTheme's implementation
aligns with the ThemeProviderState type to remove the inconsistency.

Comment thread apps/web/src/lib/theme/ThemeProvider.tsx Outdated
Comment thread apps/web/src/lib/theme/ThemeProvider.tsx Outdated
Comment thread apps/web/src/lib/theme/themes.ts
Comment on lines +4 to +10
export const useTheme = () => {
const context = useContext(ThemeProviderContext);

if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");

return context;

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.

⚠️ Potential issue | 🟠 Major

The undefined check will never trigger due to context default value.

Since ThemeProviderContext is created with initialState as the default value (in ThemeContext.ts), context will never be undefined. If useTheme is called outside a ThemeProvider, it will silently return the non-functional initialState instead of throwing.

To properly detect missing provider, either:

  1. Use undefined as the context default and update the type, or
  2. Check for a sentinel value in the initial state.
Option 1: Use undefined as the default (recommended)

In ThemeContext.ts:

-export const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
+export const ThemeProviderContext = createContext<ThemeProviderState | undefined>(undefined);

Then update the hook check accordingly (current check would work).

🤖 Prompt for AI Agents
In `@apps/web/src/lib/theme/useTheme.ts` around lines 4 - 10, The hook useTheme
currently checks for undefined but ThemeProviderContext is created with
initialState so the check never fires; update the context/provider pattern:
change ThemeProviderContext's default to undefined and its type to possibly
undefined in ThemeContext.ts (so useContext(ThemeProviderContext) can return
undefined), then keep the existing undefined check in useTheme (throwing when
context is undefined), or alternatively leave the default as initialState and
modify useTheme to detect a sentinel value from initialState (e.g., a specific
flag like isInitialized) and throw when that sentinel indicates no provider;
reference ThemeProviderContext, initialState, useTheme, and ThemeContext.ts when
making the change.

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

🤖 Fix all issues with AI agents
In `@apps/web/src/layouts/AdminLayout.spec.tsx`:
- Around line 54-58: The tests render AdminLayout with ThemeProvider using an
invalid defaultTheme value "light"; update the ThemeProvider defaultTheme prop
to a real production theme key (for example "violet-bloom") in both places where
AdminLayout is rendered so state/localStorage uses a valid theme key (check the
ThemeProvider usage in AdminLayout.spec.tsx and replace defaultTheme="light"
with defaultTheme="violet-bloom").

In `@apps/web/src/lib/theme/ThemeProvider.tsx`:
- Around line 17-38: The current logic lets an unknown theme key persist in the
theme state causing UI desync; normalize and clamp theme keys when reading from
storage and when setting theme: in the useState initializer that reads
localStorage (the theme state), validate the retrieved key against the themes
map and default to defaultTheme if missing, and also ensure any setter path
(e.g., functions that call setTheme or the applyTheme call path) validates or
maps the incoming currentThemeKey to a known key before updating state or
calling applyTheme; use the same lookup logic used in applyTheme
(themes[currentThemeKey] || themes[defaultTheme] || Object.values(themes)[0]) to
compute a canonical key and store that canonical key via setTheme so theme state
always holds a valid theme key.

Comment thread apps/web/src/layouts/AdminLayout.spec.tsx
Comment thread apps/web/src/lib/theme/ThemeProvider.tsx Outdated

@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)
apps/web/src/layouts/AdminLayout.spec.tsx (1)

70-70: Inconsistent ThemeProvider wrapping across tests.

Tests at lines 70 and 86 render AdminLayout without the ThemeProvider wrapper, while other tests include it. If AdminLayout or any descendant calls useTheme, these tests will fail with a context error.

For consistency and to prevent potential failures if component dependencies change, consider wrapping all test renders:

Suggested fix
-        render(<AdminLayout />);
+        render(
+            <ThemeProvider defaultTheme="violet-bloom" storageKey="test-theme">
+                <AdminLayout />
+            </ThemeProvider>
+        );

Apply the same pattern to the test at line 86.

🤖 Fix all issues with AI agents
In `@apps/web/src/lib/theme/ThemeProvider.tsx`:
- Around line 17-23: getValidTheme currently trusts defaultTheme and can return
an invalid key causing themes[validKey] to be undefined; update the useCallback
implementation (getValidTheme) to: 1) check if the provided key exists in themes
and return it; 2) if not, verify that defaultTheme is a valid key in themes and
return it; 3) as a final fallback return the first available theme key (e.g.
Object.keys(themes)[0]). Also ensure the useCallback dependency array includes
themes (i.e., [defaultTheme, themes]) so the validator updates when theme map
changes; this protects the subsequent use of themes[validKey] in the
ThemeProvider render path.

Comment on lines +17 to +23
// Helper to get a valid theme key
const getValidTheme = useCallback((key: string | undefined | null) => {
if (key && themes[key]) {
return key;
}
return defaultTheme;
}, [defaultTheme]);

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.

⚠️ Potential issue | 🟡 Minor

getValidTheme doesn't validate defaultTheme itself.

If defaultTheme is not a valid key in themes, getValidTheme returns it anyway, and themes[validKey] on line 40 will be undefined. Consider falling back to the first available theme as a final safety net:

Suggested fix
 const getValidTheme = useCallback((key: string | undefined | null) => {
     if (key && themes[key]) {
         return key;
     }
-    return defaultTheme;
+    if (themes[defaultTheme]) {
+        return defaultTheme;
+    }
+    // Ultimate fallback to first available theme
+    return Object.keys(themes)[0] ?? defaultTheme;
 }, [defaultTheme]);
🤖 Prompt for AI Agents
In `@apps/web/src/lib/theme/ThemeProvider.tsx` around lines 17 - 23, getValidTheme
currently trusts defaultTheme and can return an invalid key causing
themes[validKey] to be undefined; update the useCallback implementation
(getValidTheme) to: 1) check if the provided key exists in themes and return it;
2) if not, verify that defaultTheme is a valid key in themes and return it; 3)
as a final fallback return the first available theme key (e.g.
Object.keys(themes)[0]). Also ensure the useCallback dependency array includes
themes (i.e., [defaultTheme, themes]) so the validator updates when theme map
changes; this protects the subsequent use of themes[validKey] in the
ThemeProvider render path.

@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)
apps/web/src/layouts/AdminLayout.spec.tsx (1)

40-44: Consider clearing localStorage in beforeEach for test isolation.

ThemeProvider reads from localStorage on mount. While storageKey="test-theme" isolates from production, clearing localStorage between tests ensures no state leakage across test runs.

Suggested addition
     beforeEach(() => {
         vi.clearAllMocks();
+        localStorage.clear();
         (useNavigate as unknown as Mock).mockReturnValue(mockNavigate);
         (useLocation as unknown as Mock).mockReturnValue({ pathname: '/admin' });
     });
🤖 Fix all issues with AI agents
In `@apps/web/src/lib/theme/ThemeProvider.tsx`:
- Around line 90-100: The context `value` object (used in
ThemeProviderContext.Provider) is recreated each render causing needless
consumer re-renders; wrap the creation of `value` in a useMemo that depends only
on `theme` (since `setTheme` is stable and `availableThemes` can be derived from
the module-level `themes`) and pass the memoized object to
ThemeProviderContext.Provider to avoid unnecessary re-renders.
♻️ Duplicate comments (1)
apps/web/src/lib/theme/ThemeProvider.tsx (1)

28-37: Catch block bypasses theme validation.

Line 34 returns defaultTheme directly without validation, but line 32 properly validates via getValidTheme(). If localStorage throws and defaultTheme is an invalid key, the state will hold an invalid theme.

Suggested fix
         const [theme, setThemeState] = useState<string>(
             () => {
                 try {
                     const stored = localStorage.getItem(storageKey);
                     return getValidTheme(stored);
                 } catch {
-                    return defaultTheme;
+                    return getValidTheme(undefined);
                 }
             }
         );

Comment thread apps/web/src/lib/theme/ThemeProvider.tsx Outdated
@pramodnarayana
pramodnarayana merged commit 784c1f1 into development Jan 19, 2026
2 checks passed
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