feat: auto theme switching based on OS light/dark preference#260
feat: auto theme switching based on OS light/dark preference#260kayvanaarssen wants to merge 3 commits intoDavid-Crty:mainfrom
Conversation
Add an "Auto (System)" mode to the Appearance settings page that switches the active daisyUI theme automatically when the OS light/dark preference changes, alongside the existing manual single-theme picker. Changes: - Preferences component: new themeMode (manual|auto), lightTheme and darkTheme properties, stored in cookies (theme_mode, light_theme, dark_theme) alongside the existing theme cookie; new setThemeMode, setLightTheme, setDarkTheme Livewire methods - Preferences blade: "Theme Mode" card with Manual / Auto (System) toggle; Auto mode reveals two side-by-side theme pickers (Light Mode Theme, Dark Mode Theme); Manual mode shows the existing single-theme grid - layouts/_theme-init.blade.php: small inline script injected before the Vite bundle that reads cookies and sets data-theme on <html> synchronously (no flash), and registers a matchMedia change listener so the theme updates immediately when the OS preference changes without a page reload - layouts/app.blade.php and auth.blade.php: updated server-side data-theme fallback to respect the new themeMode cookie; include the theme-init script partial - settings/_theme-swatch.blade.php: extracted repeated theme preview card markup into a partial to avoid duplication
📝 WalkthroughWalkthroughAdds multi-mode theming: Livewire Preferences now tracks ChangesTheme System with Multi-Mode Support
Sequence Diagram(s)sequenceDiagram
participant Browser
participant Layout as Blade Layout
participant Livewire as Livewire Component
participant Cookies as Cookie Jar
Browser->>Layout: Request page
Layout->>Cookies: read theme_mode, theme, light_theme, dark_theme
Layout->>Browser: render HTML with meta(theme-config) + data-theme
Browser->>Browser: _theme-init runs → applyTheme() (meta + matchMedia)
Browser->>Livewire: user selects mode/theme
Livewire->>Cookies: queue theme_mode/theme/light_theme/dark_theme (1 year)
Livewire-->>Browser: component state updated (skipRender)
Browser->>Browser: _theme-init re-applies on livewire:navigated or matchMedia change
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/Livewire/Settings/Preferences.php`:
- Around line 30-40: Mount and setter methods currently accept any cookie/string
for theme names; add validation against a defined allowed set and call Livewire
validation before assigning. Define an allowedThemes list in the component and
add validation rules (e.g., for properties lightTheme and darkTheme using an
"in:..." rule or a Form/#[Validate] attribute), then in mount() validate the
cookie-derived values (or fallback to defaults) and in setLightTheme() and
setDarkTheme() call $this->validate() (or validateOnly) to enforce the allowed
list before setting $this->lightTheme and $this->darkTheme; also ensure theme
and themeMode are similarly constrained if needed. Reference: mount(),
setLightTheme(), setDarkTheme(), properties lightTheme, darkTheme, theme,
themeMode.
In `@resources/views/livewire/settings/preferences.blade.php`:
- Around line 96-103: The Blade view contains a hard-coded `@php` themes array;
move that array into the Livewire component class
app/Livewire/Settings/Preferences.php as a public property or class constant
(e.g., public $themes or const THEMES = [...]) and initialize it (directly or in
mount()) so the view can use $themes; then remove the `@php` block from
resources/views/livewire/settings/preferences.blade.php and reference the
injected $themes in the template.
- Around line 108-113: The theme swatch cards are clickable divs and need to be
keyboard-accessible; change the interactive wrapper that currently uses the div
with :class="isActive('{{ $themeName }}')" and `@click`="setTheme('{{ $themeName
}}')" into a semantic <button> (or a component that renders a button) so it is
focusable and keyboard-operable, preserve the same classes/outline logic and the
isActive('{{ $themeName }}') check, and ensure `@click`="setTheme('{{ $themeName
}}')" remains; apply the same change to the other two pickers that use the same
pattern (locations referencing isActive, setTheme and the themeName loop) so all
three pickers use buttons instead of divs.
- Around line 7-25: Both init() and setThemeMode() add a new
window.matchMedia('(prefers-color-scheme: dark)') listener when themeMode ===
'auto', which causes duplicate handlers when toggling; fix by storing the
MediaQueryList and the listener handler on the component (e.g.
this._prefMediaQuery and this._onPrefChange), remove the existing listener
before adding a new one, and ensure you remove the listener when switching out
of auto mode; update init(), setThemeMode(), and applyAutoTheme() to use these
stored symbols (this._prefMediaQuery, this._onPrefChange, init, setThemeMode,
applyAutoTheme) so only one listener is ever attached.
🪄 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: 65265a11-b0a6-4b64-9480-2c0723f00c08
📒 Files selected for processing (6)
app/Livewire/Settings/Preferences.phpresources/views/layouts/_theme-init.blade.phpresources/views/layouts/app.blade.phpresources/views/layouts/auth.blade.phpresources/views/livewire/settings/_theme-swatch.blade.phpresources/views/livewire/settings/preferences.blade.php
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.php
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.php: In PHP files, always use curly braces for control structures, even for single-line bodies.
Use PHP 8 constructor property promotion:public function __construct(public GitHub $github) { }. Do not leave empty zero-parameter__construct()methods unless the constructor is private.
Use explicit return type declarations and type hints for all method parameters in PHP:function isAccessible(User $user, ?string $path = null): bool
Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
Use array shape type definitions in PHPDoc blocks for PHP.
Files:
resources/views/layouts/auth.blade.phpresources/views/livewire/settings/_theme-swatch.blade.phpresources/views/layouts/_theme-init.blade.phpapp/Livewire/Settings/Preferences.phpresources/views/livewire/settings/preferences.blade.phpresources/views/layouts/app.blade.php
resources/views/**/*.blade.php
📄 CodeRabbit inference engine (CLAUDE.md)
resources/views/**/*.blade.php: All UI components in Mary UI should be prefixed withx-(e.g.,<x-button>,<x-input>,<x-card>) and use Heroicons for icons (e.g.,icon="o-user"for outline,icon="s-user"for solid).
In Mary UI select components, use the:optionsprop with array format[['id' => 'value', 'name' => 'Label']].
In Mary UI alerts, useclass="alert-success",class="alert-error", etc. (notvariantprop).
Use Alpine.js for client-side interactions in Livewire instead of JavaScript frameworks.
In Blade templates, use:attrbinding (dynamic syntax) instead of{{ }}interpolation when passing translated strings to component attributes to avoid double-encoding special characters.
Files:
resources/views/layouts/auth.blade.phpresources/views/livewire/settings/_theme-swatch.blade.phpresources/views/layouts/_theme-init.blade.phpresources/views/livewire/settings/preferences.blade.phpresources/views/layouts/app.blade.php
**/*.{php,blade.php}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{php,blade.php}: Always use named routes and theroute()function when generating links to other pages.
Translations should use__('...')helper function and be stored in JSON translation files inlang/{locale}.json. Keep technical terms like 'Backup', 'Restore', 'Snapshot(s)' in English across all locales.
Files:
resources/views/layouts/auth.blade.phpresources/views/livewire/settings/_theme-swatch.blade.phpresources/views/layouts/_theme-init.blade.phpapp/Livewire/Settings/Preferences.phpresources/views/livewire/settings/preferences.blade.phpresources/views/layouts/app.blade.php
resources/views/livewire/**/*.blade.php
📄 CodeRabbit inference engine (CLAUDE.md)
All Livewire component blade files should contain only view markup; all PHP logic must be in the component class.
Files:
resources/views/livewire/settings/_theme-swatch.blade.phpresources/views/livewire/settings/preferences.blade.php
app/Livewire/**/*.php
📄 CodeRabbit inference engine (CLAUDE.md)
app/Livewire/**/*.php: In Livewire components, use#[Validate]attributes or Form objects for validation. Call$this->validate()before processing data.
In Livewire components, useSession::flash()for one-time messages and show them via@if (session('success'))in Blade templates.
In Livewire components, return$this->redirect()withnavigate: truefor SPA-like navigation.
In Mary UI modals, add a boolean property to the component class and usewire:modelin the Blade template.
Keep state server-side in Livewire components so the UI reflects it. Validate and authorize in actions as you would in HTTP requests.
Files:
app/Livewire/Settings/Preferences.php
🧠 Learnings (4)
📚 Learning: 2026-01-30T22:27:46.107Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 61
File: resources/views/livewire/volume/connectors/s3-config.blade.php:1-13
Timestamp: 2026-01-30T22:27:46.107Z
Learning: In Blade template files (any .blade.php) within the databasement project, allow using alert-info for informational content inside <x-alert> components. The guideline that permits alert-success and alert-error does not exclude using alert-info for informational purposes. Apply this consistently to all Blade components that render alerts; ensure semantic usage and accessibility.
Applied to files:
resources/views/layouts/auth.blade.phpresources/views/livewire/settings/_theme-swatch.blade.phpresources/views/layouts/_theme-init.blade.phpresources/views/livewire/settings/preferences.blade.phpresources/views/layouts/app.blade.php
📚 Learning: 2026-02-06T10:34:43.585Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 75
File: resources/views/livewire/backup-job/_filters.blade.php:36-40
Timestamp: 2026-02-06T10:34:43.585Z
Learning: In Blade template files, when creating compact inline filter controls, prefer using native <input type="checkbox"> elements with daisyUI classes (e.g., checkbox checkbox-warning checkbox-xs) over the Mary UI <x-checkbox> component. The <x-checkbox> component adds wrapper markup (e.g., <div><fieldset><label> with gap-3) that can break tight inline flex layouts. Use the native input approach for compact inline controls, but reserve <x-checkbox> for form fields that require labels, hints, and errors.
Applied to files:
resources/views/layouts/auth.blade.phpresources/views/livewire/settings/_theme-swatch.blade.phpresources/views/layouts/_theme-init.blade.phpresources/views/livewire/settings/preferences.blade.phpresources/views/layouts/app.blade.php
📚 Learning: 2026-02-25T10:48:17.811Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 132
File: app/Console/Commands/RecoverAgentLeasesCommand.php:44-48
Timestamp: 2026-02-25T10:48:17.811Z
Learning: When reviewing PHP code, especially with foreign keys that use cascadeOnDelete and are non-nullable, assume child relations exist at runtime (the database will delete children when the parent is deleted). Do not rely on null-safe operators for these relations, as PHPStan already models them as non-null. This guideline applies broadly to PHP files that define models with foreign keys using cascade delete; verify there are no unnecessary null checks or optional chaining on such relations.
Applied to files:
resources/views/layouts/auth.blade.phpresources/views/livewire/settings/_theme-swatch.blade.phpresources/views/layouts/_theme-init.blade.phpapp/Livewire/Settings/Preferences.phpresources/views/livewire/settings/preferences.blade.phpresources/views/layouts/app.blade.php
📚 Learning: 2026-02-18T09:45:52.485Z
Learnt from: David-Crty
Repo: David-Crty/databasement PR: 116
File: app/Livewire/DatabaseServer/ConnectionStatus.php:18-18
Timestamp: 2026-02-18T09:45:52.485Z
Learning: In Livewire components, Eloquent model properties (e.g., public DatabaseServer $server) are automatically locked by the framework to prevent client-side ID tampering. The #[Locked] attribute is only needed for scalar properties (int, string, bool, etc.) that require protection from client-side mutation. Apply this guidance to all Livewire PHP components; use #[Locked] only on primitive properties that you want to shield from client manipulation, and rely on automatic locking for Eloquent model properties.
Applied to files:
app/Livewire/Settings/Preferences.php
🔇 Additional comments (4)
resources/views/livewire/settings/_theme-swatch.blade.php (1)
1-23: Clean partial extraction; looks good.This is a solid reusable swatch partial and keeps the markup compact.
resources/views/layouts/app.blade.php (1)
2-8: Theme bootstrap placement is correct.Including the initializer before
@viteand setting an initialdata-themeserver-side is the right direction for reducing first-paint mismatch.Also applies to: 16-16
resources/views/layouts/auth.blade.php (1)
2-8: Auth layout parity is good.Good to see the same theme-init flow applied here so auth screens behave like the main app.
Also applies to: 16-16
resources/views/layouts/_theme-init.blade.php (1)
1-29: Initializer behavior matches the new theme model.Cookie defaults and auto/manual branching are consistent with the Livewire preference state model.
Cookies are encrypted by Laravel so cannot be read by JavaScript. Instead, inject theme config from PHP (which decrypts cookies correctly) into a <meta name="theme-config"> tag in <head>. The _theme-init script reads the meta tag and re-applies the correct theme on livewire:navigated and on OS preference changes.
- Add ALLOWED_THEMES constant and validate theme names in mount(), setTheme(), setLightTheme(), setDarkTheme() to reject invalid values - Move themes array from @php blade block into component render() method - Fix duplicate matchMedia listeners by storing handler reference and calling removeEventListener before re-attaching in setThemeMode() - Replace clickable divs with semantic <button type="button"> elements and add :aria-pressed bindings for keyboard accessibility
Summary
theme_mode,light_theme,dark_theme) — no database migration needed.How it works
A small inline script (
layouts/_theme-init.blade.php) is injected into<head>before the Vite bundle sodata-themeis set synchronously on<html>before any CSS paints (no flash of wrong theme on page load). It also registers amatchMediachange listener so the theme updates live when the OS preference changes.Changes
app/Livewire/Settings/Preferences.phpthemeMode,lightTheme,darkThemeproperties + settersresources/views/livewire/settings/preferences.blade.phpresources/views/livewire/settings/_theme-swatch.blade.phpresources/views/layouts/_theme-init.blade.phpresources/views/layouts/app.blade.phpresources/views/layouts/auth.blade.phpTest plan
make test)🤖 Generated with Claude Code
Summary by CodeRabbit
Screenshot: