20 KiB
Dark Mode Support Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add cookie-persisted dark/light theming with a floating top-right toggle on all regular views, while keeping export always light.
Architecture: Theme behavior is centralized in one reusable partial with two include modes (head and body) so every themed page gets identical bootstrap, cookie logic, and toggle UI. Existing views keep structure/layout classes and only adjust color-related classes needed for dark-mode readability. Export is explicitly excluded and remains light-only.
Tech Stack: PHP 8, server-rendered PHP views, Tailwind via CDN, Bootstrap Icons, vanilla JavaScript, browser cookies.
Chunk 1: Shared Theme Unit and Core Integration
Task 1: Create shared theme partial with explicit section contract
Files:
-
Create:
views/partials/theme.php -
Test: browser manual validation on
/loginafter integration -
Step 1: Add section-gated partial skeleton
<?php
$themeSection = $themeSection ?? null;
if ($themeSection === 'head') {
// output head-only theme bootstrap and shared CSS
} elseif ($themeSection === 'body') {
// output body-only floating toggle and behavior script
}
- Step 2: Implement
headoutput with early theme bootstrap script
<script>
(function() {
function readThemeCookie() {
var match = document.cookie.match(/(?:^|; )theme=([^;]+)/);
if (!match) return null;
var value = decodeURIComponent(match[1]);
return (value === 'dark' || value === 'light') ? value : null;
}
var cookieTheme = readThemeCookie();
var systemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var resolved = cookieTheme || (systemDark ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', resolved === 'dark');
document.documentElement.setAttribute('data-theme', resolved);
})();
</script>
Expected output:
-
<html>hasdarkclass before body render when resolved theme is dark. -
data-themeis set todarkorlight. -
Step 3: Implement
headshared CSS tokens and semantic utility classes
<style>
:root {
--theme-bg: #f3f4f6;
--theme-surface: #ffffff;
--theme-surface-alt: #f9fafb;
--theme-text: #1f2937;
--theme-text-muted: #4b5563;
--theme-border: #d1d5db;
}
html.dark {
--theme-bg: #0f172a;
--theme-surface: #111827;
--theme-surface-alt: #1f2937;
--theme-text: #e5e7eb;
--theme-text-muted: #9ca3af;
--theme-border: #374151;
}
.theme-page { background-color: var(--theme-bg); color: var(--theme-text); }
.theme-surface { background-color: var(--theme-surface); color: var(--theme-text); }
.theme-surface-alt { background-color: var(--theme-surface-alt); }
.theme-text { color: var(--theme-text); }
.theme-text-muted { color: var(--theme-text-muted); }
.theme-border { border-color: var(--theme-border); }
.theme-input { background-color: var(--theme-surface); color: var(--theme-text); border-color: var(--theme-border); }
#themeToggle {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 50;
}
#themeToggle:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
</style>
Expected output:
-
Shared classes exist for page/surface/text/border/input mappings.
-
Toggle has fixed top-right placement and z-index 50.
-
Step 4: Implement
bodyoutput with toggle markup and no-JS fallback
<button id="themeToggle" type="button" aria-label="Switch to dark mode" title="Switch to dark mode">
<i id="themeToggleIcon" class="bi bi-moon-stars-fill"></i>
</button>
<noscript><style>#themeToggle{display:none;}</style></noscript>
- Step 5: Implement
bodybehavior script with cookie helpers and state sync
<script>
(function() {
var toggle = document.getElementById('themeToggle');
var icon = document.getElementById('themeToggleIcon');
function readThemeCookie() {
var match = document.cookie.match(/(?:^|; )theme=([^;]+)/);
if (!match) return null;
var value = decodeURIComponent(match[1]);
return (value === 'dark' || value === 'light') ? value : null;
}
function writeThemeCookie(theme) {
document.cookie = 'theme=' + encodeURIComponent(theme) + '; max-age=31536000; path=/; SameSite=Lax';
}
function applyTheme(theme) {
var isDark = theme === 'dark';
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.setAttribute('data-theme', theme);
}
function syncToggle(theme) {
var isDark = theme === 'dark';
icon.className = isDark ? 'bi bi-sun-fill' : 'bi bi-moon-stars-fill';
var nextAction = isDark ? 'Switch to light mode' : 'Switch to dark mode';
toggle.setAttribute('aria-label', nextAction);
toggle.setAttribute('title', nextAction);
}
var currentTheme = document.documentElement.classList.contains('dark') ? 'dark' : (readThemeCookie() || 'light');
syncToggle(currentTheme);
toggle.addEventListener('click', function() {
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(currentTheme);
writeThemeCookie(currentTheme);
syncToggle(currentTheme);
});
})();
</script>
Expected output:
-
Click toggles theme immediately, updates icon,
aria-label, andtitle. -
Cookie write attempt does not block visual switch if persistence is restricted.
-
Step 6: Run syntax check for new partial
Run: php -l views/partials/theme.php
Expected: No syntax errors detected in views/partials/theme.php.
- Step 7: Commit Task 1
git add views/partials/theme.php
git commit -m "feat: add shared theme partial with cookie-based dark mode"
Task 2: Integrate theme partial into login and setup views
Files:
-
Modify:
views/login.php -
Modify:
views/setup.php -
Test:
/login,/setup -
Step 1: Insert head include in
views/login.php
<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>
Placement: in <head>, after Tailwind/bootstrap includes, before </head>.
- Step 2: Insert body include in
views/login.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>
Placement: before </body>.
- Step 3: Apply color-only updates in
views/login.php
Apply concrete replacements:
<body class="bg-gray-100 ...">-><body class="theme-page ...">- login card
bg-white->theme-surface - heading
text-gray-800->theme-text - label text
text-gray-700->theme-text - input
border-gray-300-> addtheme-input theme-border
Expected outcome: login card, labels, and input fields are readable in both themes without layout shift.
- Step 4: Insert head include in
views/setup.php
<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>
Placement: in <head>, after Tailwind/bootstrap includes, before </head>.
- Step 5: Insert body include in
views/setup.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>
Placement: before </body>.
- Step 6: Apply color-only updates in
views/setup.php
Apply concrete replacements:
<body class="bg-gray-100 ...">-><body class="theme-page ...">- setup card
bg-white->theme-surface - title and labels
text-gray-*->theme-text/theme-text-muted - password inputs
border-gray-300-> addtheme-input theme-border
Expected outcome: setup page remains visually consistent, with readable text/inputs in both themes.
- Step 7: Run syntax checks for auth/setup views
Run: php -l views/login.php && php -l views/setup.php
Expected: both report No syntax errors detected.
- Step 8: Manual verification on
/loginand/setup
Expected:
-
toggle appears fixed top-right
-
keyboard Tab focuses toggle with visible focus style
-
Enter/Space toggles theme
-
icon and
aria-label/titlechange each toggle -
reload preserves preference
-
Step 9: Commit Task 2
git add views/login.php views/setup.php
git commit -m "feat: integrate dark mode toggle on auth and setup views"
Chunk 2: Main Views, Export Exception, and Final Verification
Task 3: Integrate theme partial into home view
Files:
-
Modify:
views/index.php -
Test:
/home -
Step 1: Add head include in
views/index.php
<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>
Placement requirement: after Tailwind/bootstrap includes and before </head>.
Expected outcome: exactly one head include in required position.
- Step 2: Add body include in
views/index.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>
Placement requirement: before local page scripts and before </body>.
Expected outcome: exactly one body include in required position.
- Step 3: Update page and header surfaces in
views/index.php
Concrete updates:
- body wrapper
bg-gray-100->theme-page - header/card wrappers
bg-white->theme-surface - heading/body text
text-gray-*->theme-text/theme-text-muted
Expected outcome: page shell and header are readable in both themes.
- Step 4: Update table surfaces in
views/index.php
Concrete updates:
- table header
<thead class="bg-gray-50 border-b">-> usetheme-surface-alt theme-border - table body divider
divide-gray-200->theme-border - table cell text classes
text-gray-500,text-gray-600,text-gray-900->theme-text/theme-text-muted
Expected outcome: table head/body text and borders remain readable in both themes.
- Step 5: Update vehicle cards/list rows in
views/index.php
Concrete updates:
- grid cards
bg-white->theme-surface - list container
bg-white->theme-surface - row hover
hover:bg-gray-50->hover:opacity-95and applytheme-surface-alton row container where needed - small metadata text
text-gray-*->theme-text-muted
Expected outcome: vehicle cards/list rows remain readable in both themes.
- Step 6: Update modal container and input surfaces in
views/index.php
Concrete updates:
- modal panel
bg-white->theme-surface - modal close/cancel gray text ->
theme-text-muted - inputs with
border-gray-300-> addtheme-input theme-border - optional helper text gray shades ->
theme-text-muted
Expected outcome: cards, table, and modal inputs remain readable/usable in both themes.
- Step 7: Verify and adjust flash message contrast in
views/index.php
Concrete updates:
- keep existing light-mode classes for base state (
bg-red-100 border-red-400 text-red-700,bg-green-100 border-green-400 text-green-700) - add dark-only variants (using
dark:classes or equivalent conditional class strategy) for error/success contrast in dark mode - keep existing rounded/padding/layout classes unchanged
Expected outcome: error and success flash messages are legible and visually distinct in both light and dark modes.
- Step 8: Run syntax check for
views/index.php
Run: php -l views/index.php
Expected: No syntax errors detected in views/index.php.
- Step 9: Manual verification for
/home
Expected:
-
theme toggles correctly
-
theme persists after reload
-
cards/tables/modals remain readable in both themes
-
Step 10: Commit Task 3
git add views/index.php
git commit -m "feat: integrate dark mode support in home view"
Task 4: Integrate theme partial into vehicle detail view
Files:
-
Modify:
views/vehicle.php -
Test:
/vehicles/{id} -
Step 1: Add head include in
views/vehicle.php
<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>
Placement requirement: after Tailwind/bootstrap includes and before </head>.
Expected outcome: exactly one head include in required position.
- Step 2: Add body include in
views/vehicle.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>
Placement requirement: before page-local scripts and before </body>.
Expected outcome: exactly one body include in required position.
- Step 3: Update page shell, vehicle card, and maintenance form in
views/vehicle.php
Concrete updates:
- body/header/card backgrounds
bg-white/bg-gray-*-> theme classes - text grays on headings/body ->
theme-text/theme-text-muted - form inputs/borders ->
theme-input theme-border
Expected outcome: top section and add-maintenance form are readable in both themes.
- Step 4: Update dropdown menu surfaces in
views/vehicle.php
Concrete updates:
- suggestions box
bg-white border-gray-300->theme-surface theme-border - export menu panel
bg-white->theme-surface - export menu links
hover:bg-gray-100->hover:opacity-95 - export menu link text grays ->
theme-text
Expected outcome: dropdown menus remain readable and interactive in both themes.
- Step 5: Update maintenance history surfaces in
views/vehicle.php
Concrete updates:
- history container
bg-white->theme-surface - item cards
border-gray-200->theme-border - item card hover
hover:border-blue-300->hover:border-blue-500 - item metadata text gray shades ->
theme-text-muted
Expected outcome: maintenance history cards remain readable in both themes.
- Step 6: Update modal panel and input surfaces in
views/vehicle.php
Concrete updates:
- edit modal panels
bg-white->theme-surface - modal input
border-gray-300->theme-input theme-border - modal close/cancel text grays ->
theme-text-muted
Expected outcome: dropdowns, history cards, and modals remain readable and interactive in both themes.
- Step 7: Run syntax check for
views/vehicle.php
Run: php -l views/vehicle.php
Expected: No syntax errors detected in views/vehicle.php.
- Step 8: Manual verification for
/vehicles/{id}
Expected:
-
theme toggles and persists
-
forms/dropdowns/modals remain readable in both themes
-
navigation to/from
/homekeeps cookie-based preference -
Step 9: Commit Task 4
git add views/vehicle.php
git commit -m "feat: integrate dark mode support in vehicle detail view"
Task 5: Integrate theme partial into settings view
Files:
-
Modify:
views/settings.php -
Test:
/settings -
Step 1: Add head include in
views/settings.php
<?php $themeSection = 'head'; include __DIR__ . '/partials/theme.php'; ?>
Placement requirement: after Tailwind/bootstrap includes and before </head>.
Expected outcome: exactly one head include in required position.
- Step 2: Add body include in
views/settings.php
<?php $themeSection = 'body'; include __DIR__ . '/partials/theme.php'; ?>
Placement requirement: before local scripts (if any) and before </body>.
Expected outcome: exactly one body include in required position.
- Step 3: Update page/header/card surfaces in
views/settings.php
Concrete updates:
- body/header/cards
bg-white/bg-gray-*->theme-page/theme-surface/theme-surface-alt - gray text classes ->
theme-text/theme-text-muted
Expected outcome: page shell and cards remain readable in both themes.
- Step 4: Update list rows and password form surfaces in
views/settings.php
Concrete updates:
- quick-task row hover/background and borders -> theme classes
- password input borders/backgrounds ->
theme-input theme-border
Expected outcome: list rows and form fields are readable and distinguishable in both themes.
- Step 5: Verify action/footer contrast in
views/settings.php
Concrete updates:
- logout action danger colors may be adjusted if needed for dark-mode contrast while preserving danger semantics
- powered footer card
bg-white->theme-surface - powered footer title/body grays ->
theme-text/theme-text-muted - GitHub icon/link muted grays ->
theme-text-mutedwhile preserving blue link emphasis
Expected outcome: call-to-action and footer text remain legible in both themes.
- Step 6: Run syntax check for
views/settings.php
Run: php -l views/settings.php
Expected: No syntax errors detected in views/settings.php.
- Step 7: Manual verification for
/settings
Expected:
-
theme toggles and persists
-
cards/list rows/inputs remain readable in both themes
-
navigation to/from
/homekeeps cookie-based preference -
Step 8: Commit Task 5
git add views/settings.php
git commit -m "feat: integrate dark mode support in settings view"
Task 6: Validate export exception and complete end-to-end checks
Files:
-
Verify unchanged:
views/export.php -
Verify behavior:
views/partials/theme.php -
Verify integrated views:
views/login.php,views/setup.php,views/index.php,views/vehicle.php,views/settings.php -
Step 1: Verify
views/export.phphas no theme include/toggle
Expected:
-
no
$themeSectioninclude -
no
#themeTogglemarkup -
no dark-mode bootstrap script and no
html.darkapplication path in export template -
Step 2: Verify cookie contract in devtools
Flow:
- toggle theme on any themed page
- inspect cookie attributes
Expected:
-
cookie name
theme -
value
darkorlight -
Max-Age=31536000 -
Path=/ -
SameSite=Lax -
Step 3: Verify accessibility state updates on toggle
Flow:
- focus toggle via keyboard
- activate twice
Expected per click:
-
icon swaps moon/sun
-
aria-labelandtitleupdate to next action -
Step 4: Verify export is always light
Flow:
- set
theme=dark - open
/vehicles/{id}/export/html
Expected:
-
export remains light styled
-
toggle not present
-
<html>does not receivedarkclass on export, even whentheme=dark -
Step 5: Verify toggle stays visible above modals/dropdowns
Flow:
- open each page with modal/dropdown support (
/home,/vehicles/{id}) - open add/edit modals and export/suggestions dropdowns
Expected:
-
floating toggle remains visible and clickable above overlays
-
Step 5a: Apply layering remediation if Step 5 fails
Flow:
- if toggle is hidden or unclickable over overlays, adjust stacking order in
views/partials/theme.php
Concrete remediation:
- increase
#themeTogglez-index above conflicting overlay layer - if conflict persists, reduce non-critical overlay z-index where safe without breaking modal usability
Expected:
-
toggle remains visible/clickable while modals/dropdowns still function correctly
-
Step 6: Verify no-cookie fallback uses system preference
Flow:
- delete
themecookie - load
/home
Expected:
-
resolved theme follows current
prefers-color-scheme -
Step 7: Verify invalid-cookie fallback
Flow:
- set
theme=invalid - reload
/home
Expected:
-
invalid cookie ignored
-
resolved theme follows current system preference
-
Step 8: Verify no-JS fallback
Flow:
- disable JS in browser
- open
/login
Expected:
-
light mode only
-
no visible floating toggle
-
login form remains usable
-
Step 9: Verify cookie-write-failure behavior
Flow:
- use a browser mode/policy that blocks cookie writes (or simulate via devtools/privacy settings)
- click toggle on
/home
Expected:
-
theme still switches immediately on the current page
-
no error message is shown to the user
-
persistence across reload is not required in this scenario
-
Step 10: Run final PHP syntax sweep
Run:
php -l views/partials/theme.php && \
php -l views/login.php && \
php -l views/setup.php && \
php -l views/index.php && \
php -l views/vehicle.php && \
php -l views/settings.php
Expected: all report No syntax errors detected.
- Step 11: No additional commit for verification-only task
Expected:
- Task 6 records verification results only.
- No new commit is created unless this task introduces actual file changes.