Skip to content

feat: auto theme switching based on OS light/dark preference#260

Open
kayvanaarssen wants to merge 3 commits intoDavid-Crty:mainfrom
kayvanaarssen:feature/auto-theme-system-preference
Open

feat: auto theme switching based on OS light/dark preference#260
kayvanaarssen wants to merge 3 commits intoDavid-Crty:mainfrom
kayvanaarssen:feature/auto-theme-system-preference

Conversation

@kayvanaarssen
Copy link
Copy Markdown

@kayvanaarssen kayvanaarssen commented May 2, 2026

Summary

  • Adds an Auto (System) theme mode to the Appearance settings page that automatically switches between two user-selected daisyUI themes when the OS light/dark preference changes — no page reload required.
  • When Manual is selected the existing single-theme grid works exactly as before, so there is zero regression for current users.
  • Theme preference is stored in cookies (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 so data-theme is set synchronously on <html> before any CSS paints (no flash of wrong theme on page load). It also registers a matchMedia change listener so the theme updates live when the OS preference changes.

Changes

File What changed
app/Livewire/Settings/Preferences.php Added themeMode, lightTheme, darkTheme properties + setters
resources/views/livewire/settings/preferences.blade.php New "Theme Mode" card (Manual / Auto toggle) and dual pickers for Auto mode
resources/views/livewire/settings/_theme-swatch.blade.php Extracted repeated theme preview card into a partial
resources/views/layouts/_theme-init.blade.php New inline script partial — cookie-reads + matchMedia listener
resources/views/layouts/app.blade.php Include theme-init script; server-side data-theme respects new cookies
resources/views/layouts/auth.blade.php Same as above

Test plan

  • Select Auto (System) — theme switches immediately to the appropriate picker choice based on current OS setting
  • While on Auto, toggle OS dark/light mode — theme updates without a page reload
  • Change the Light or Dark picker while in Auto mode — active theme updates immediately
  • Switch back to Manual — single picker is shown and applying a theme works
  • Reload the page in each mode — no flash of wrong theme
  • New user (no cookies) — defaults to Manual / dark, same as before
  • All 897 existing tests pass (make test)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added theme mode selection: Manual or Auto (system-driven).
    • Auto mode supports distinct light and dark themes with separate pickers.
    • Theme swatch previews for visual selection.
    • Theme choices persist for one year and apply immediately on load.
    • Client-side initializer ensures theme is set on first paint, on navigation, and when OS color-scheme changes.

Screenshot:

image

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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

Adds multi-mode theming: Livewire Preferences now tracks themeMode, lightTheme, and darkTheme (cookies + setters). Blade layouts expose theme metadata and include a client initializer script to set/respond to document.documentElement[data-theme]. Preferences UI and a reusable theme-swatch partial support Manual and Auto (system) modes.

Changes

Theme System with Multi-Mode Support

Layer / File(s) Summary
Data Shape & Initialization
app/Livewire/Settings/Preferences.php
Adds ALLOWED_THEMES allowlist and public properties themeMode, lightTheme, darkTheme. mount() reads and validates theme_mode, light_theme, dark_theme, and theme cookies with fallbacks.
Core Server Wiring
app/Livewire/Settings/Preferences.php
Adds setThemeMode(string), setLightTheme(string), setDarkTheme(string) with allowlist validation; updates state, queues cookies for 1 year, and calls skipRender(). setTheme() now validates against allowlist. render() supplies themes => ALLOWED_THEMES.
Client-side Initialization
resources/views/layouts/_theme-init.blade.php
New inline script applyTheme() reads meta[name="theme-config"], picks theme by data-mode (auto uses prefers-color-scheme), sets document.documentElement[data-theme], re-applies on livewire:navigated, and listens for OS preference changes.
Layout Integration
resources/views/layouts/app.blade.php, resources/views/layouts/auth.blade.php
Compute $initialTheme from theme_mode + theme/dark_theme cookies; set <html data-theme> to $initialTheme; add <meta name="theme-config" ...> and include _theme-init in head before assets.
UI / View Layer
resources/views/livewire/settings/preferences.blade.php
Alpine state expanded to themeMode, lightTheme, darkTheme, and currentTheme. Adds Theme Mode selector (manual/auto); conditional manual picker or separate light/dark pickers when auto. Calls new Livewire setters and uses swatch partial.
Reusable Partial
resources/views/livewire/settings/_theme-swatch.blade.php
New partial rendering clickable theme swatches keyed by theme name with four color samples; used by preferences view.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I nibble cookies, dark and light,
Manual by day, auto by night.
A hop, a swatch, the theme takes flight,
Meta whispers which shade is right,
Cookies tuck preferences tight.

🚥 Pre-merge checks | ✅ 4 | ❌ 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 (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main feature: adding automatic theme switching based on OS preference, which is the core functionality introduced across the Livewire component, layouts, and inline script.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between d07c66d and f8714b5.

📒 Files selected for processing (6)
  • app/Livewire/Settings/Preferences.php
  • resources/views/layouts/_theme-init.blade.php
  • resources/views/layouts/app.blade.php
  • resources/views/layouts/auth.blade.php
  • resources/views/livewire/settings/_theme-swatch.blade.php
  • resources/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.php
  • resources/views/livewire/settings/_theme-swatch.blade.php
  • resources/views/layouts/_theme-init.blade.php
  • app/Livewire/Settings/Preferences.php
  • resources/views/livewire/settings/preferences.blade.php
  • resources/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 with x- (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 :options prop with array format [['id' => 'value', 'name' => 'Label']].
In Mary UI alerts, use class="alert-success", class="alert-error", etc. (not variant prop).
Use Alpine.js for client-side interactions in Livewire instead of JavaScript frameworks.
In Blade templates, use :attr binding (dynamic syntax) instead of {{ }} interpolation when passing translated strings to component attributes to avoid double-encoding special characters.

Files:

  • resources/views/layouts/auth.blade.php
  • resources/views/livewire/settings/_theme-swatch.blade.php
  • resources/views/layouts/_theme-init.blade.php
  • resources/views/livewire/settings/preferences.blade.php
  • resources/views/layouts/app.blade.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{php,blade.php}: Always use named routes and the route() function when generating links to other pages.
Translations should use __('...') helper function and be stored in JSON translation files in lang/{locale}.json. Keep technical terms like 'Backup', 'Restore', 'Snapshot(s)' in English across all locales.

Files:

  • resources/views/layouts/auth.blade.php
  • resources/views/livewire/settings/_theme-swatch.blade.php
  • resources/views/layouts/_theme-init.blade.php
  • app/Livewire/Settings/Preferences.php
  • resources/views/livewire/settings/preferences.blade.php
  • resources/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.php
  • resources/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, use Session::flash() for one-time messages and show them via @if (session('success')) in Blade templates.
In Livewire components, return $this->redirect() with navigate: true for SPA-like navigation.
In Mary UI modals, add a boolean property to the component class and use wire:model in 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.php
  • resources/views/livewire/settings/_theme-swatch.blade.php
  • resources/views/layouts/_theme-init.blade.php
  • resources/views/livewire/settings/preferences.blade.php
  • resources/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.php
  • resources/views/livewire/settings/_theme-swatch.blade.php
  • resources/views/layouts/_theme-init.blade.php
  • resources/views/livewire/settings/preferences.blade.php
  • resources/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.php
  • resources/views/livewire/settings/_theme-swatch.blade.php
  • resources/views/layouts/_theme-init.blade.php
  • app/Livewire/Settings/Preferences.php
  • resources/views/livewire/settings/preferences.blade.php
  • resources/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 @vite and setting an initial data-theme server-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.

Comment thread app/Livewire/Settings/Preferences.php Outdated
Comment thread resources/views/livewire/settings/preferences.blade.php
Comment thread resources/views/livewire/settings/preferences.blade.php Outdated
Comment thread resources/views/livewire/settings/preferences.blade.php
Kay van Aarssen added 2 commits May 2, 2026 12:02
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
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