Added agentic files.

This commit is contained in:
2026-05-01 15:58:04 -04:00
parent a1d6afe503
commit b9f1a115d2
3 changed files with 1021 additions and 0 deletions
@@ -0,0 +1,642 @@
# 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 `/login` after integration
- [ ] **Step 1: Add section-gated partial skeleton**
```php
<?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 `head` output with early theme bootstrap script**
```php
<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>` has `dark` class before body render when resolved theme is dark.
- `data-theme` is set to `dark` or `light`.
- [ ] **Step 3: Implement `head` shared CSS tokens and semantic utility classes**
```php
<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 `body` output with toggle markup and no-JS fallback**
```php
<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 `body` behavior script with cookie helpers and state sync**
```php
<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`, and `title`.
- 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**
```bash
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
<?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
<?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` -> add `theme-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
<?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
<?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` -> add `theme-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 `/login` and `/setup`**
Expected:
- toggle appears fixed top-right
- keyboard Tab focuses toggle with visible focus style
- Enter/Space toggles theme
- icon and `aria-label`/`title` change each toggle
- reload preserves preference
- [ ] **Step 9: Commit Task 2**
```bash
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
<?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
<?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">` -> use `theme-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-95` and apply `theme-surface-alt` on 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` -> add `theme-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**
```bash
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
<?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
<?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 `/home` keeps cookie-based preference
- [ ] **Step 9: Commit Task 4**
```bash
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
<?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
<?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-muted` while 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 `/home` keeps cookie-based preference
- [ ] **Step 8: Commit Task 5**
```bash
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.php` has no theme include/toggle**
Expected:
- no `$themeSection` include
- no `#themeToggle` markup
- no dark-mode bootstrap script and no `html.dark` application 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 `dark` or `light`
- `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-label` and `title` update 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 receive `dark` class on export, even when `theme=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 `#themeToggle` z-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 `theme` cookie
- 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:
```bash
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.
@@ -0,0 +1,223 @@
# Dark Mode Support Design
Date: 2026-05-01
Project: Mainty
Status: Draft approved by user for implementation planning
## Goal
Add dark mode support across all regular app views with a floating dark/light toggle in the top-right corner at all times (for normal JS-enabled operation). Theme should default to the system preference when no user preference exists, and user preference should persist in a cookie.
## In Scope
- Add theme resolution and apply logic for:
- `views/index.php`
- `views/vehicle.php`
- `views/settings.php`
- `views/login.php`
- `views/setup.php`
- Add a floating top-right theme toggle visible on all in-scope views.
- Persist user-selected theme in a cookie.
- Apply cookie preference on view load.
## Out of Scope
- No database persistence for theme preference.
- No account-level sync across devices.
- No changes to route/controller/model logic.
- No dark mode support for export/print page.
## Explicit Product Decisions
- Export page remains always light for print readability:
- `views/export.php` must ignore cookie and system theme.
- If JavaScript is disabled, app falls back to light theme.
## Architecture
## 1) Shared Theme Partial
Create a reusable partial: `views/partials/theme.php`.
Responsibilities:
- Inject minimal theme bootstrap script in `<head>` to resolve theme and apply `dark` class on `<html>` as early as possible.
- Inject floating theme toggle markup near the start/end of `<body>`.
- Inject shared theme CSS variables and a concise set of utility mappings for common surfaces and text colors.
- Expose small JS helpers for:
- reading cookie,
- writing cookie,
- resolving effective theme,
- updating UI state/icon/labels,
- toggling theme on click.
Why:
- Keeps behavior consistent across all views.
- Avoids duplicating script/CSS/button logic in each template.
- Reduces maintenance cost for future theme adjustments.
Interface contract:
- The partial is included with an explicit section selector:
- `$themeSection = 'head'; include __DIR__ . '/partials/theme.php';`
- `$themeSection = 'body'; include __DIR__ . '/partials/theme.php';`
- `'head'` section outputs only bootstrap script + shared theme styles.
- `'body'` section outputs only floating toggle markup + behavior wiring script.
- Any other/missing section outputs nothing (safe no-op).
## 2) View Integration Points
Integrate the partial into each in-scope view.
- Include contract per themed view:
- In `<head>` (after Tailwind CDN script/link and before `</head>`): set `$themeSection = 'head'` and include `views/partials/theme.php`.
- In `<body>` (before page-local scripts and before `</body>`): set `$themeSection = 'body'` and include `views/partials/theme.php`.
- Partial integration must not require any page-specific IDs.
- Partial must be self-contained and safe to include on all in-scope views without per-view conditionals.
Each view keeps existing layout and structure; only color semantics are adjusted where required for dark compatibility.
## 3) Export Exception
`views/export.php` remains standalone light styling:
- No theme partial include.
- No floating toggle.
- No dark class application.
## Data Flow
## Initial Load
1. Parse cookie `theme`.
2. If cookie is `dark` or `light`, use it.
3. Else query `window.matchMedia('(prefers-color-scheme: dark)')`.
4. Apply result by toggling `document.documentElement.classList` with `dark`.
5. Render page using applied theme classes/variables.
## User Toggle Interaction
1. User clicks floating toggle.
2. Determine next theme (`light` -> `dark`, `dark` -> `light`).
3. Update `dark` class on `<html>` immediately.
4. Attempt to write cookie `theme=<dark|light>` with long-lived expiration, `path=/`, `SameSite=Lax`.
5. Update toggle icon/accessible label/title to reflect new mode.
6. If cookie persistence fails (blocked by browser/privacy settings), keep the switched theme for the current document and do not show an error; next navigation/load falls back to normal resolution flow (system preference if no valid cookie).
## Subsequent Loads
- Cookie value takes precedence over system preference.
- Users with no valid cookie continue following system theme.
## Theme Styling Strategy
Use a hybrid of CSS variables + targeted Tailwind class updates.
- Keep existing spacing/layout/structure classes.
- Introduce semantic surface/text variable mappings for repeated patterns.
- Update key hardcoded light-only color usages in templates where needed:
- page backgrounds,
- headers/cards/panels,
- tables (head/body row hover),
- forms and inputs,
- modals/dropdowns,
- flash messages.
Acceptance scope boundary for theming edits:
- Only update color-related classes/styles needed for readability/contrast in dark mode.
- Do not change spacing, typography scale, layout structure, or route/form behavior.
- A view is considered complete when all listed surface types in that view remain readable and interactive in both light and dark modes.
Action colors (primary blue, danger red) remain recognizable but are adjusted for contrast in dark contexts.
## Component Requirements
## Floating Toggle Button
- Position: fixed top-right, always visible in viewport.
- Layering: `z-index: 50`, intended to remain visible above regular page content and app modals.
- Visual states:
- sun icon when dark mode is active (indicates switch to light),
- moon icon when light mode is active (indicates switch to dark).
- Accessibility:
- semantic `<button type="button">`,
- `aria-label` updates per state,
- visible focus ring,
- keyboard operable by default.
## Cookie Contract
- Name: `theme`
- Allowed values: `dark`, `light`
- Ignore any other value as invalid.
- Attributes:
- `path=/`
- `SameSite=Lax`
- expiration `max-age=31536000` (1 year)
## Error Handling and Edge Cases
- Invalid cookie value -> treat as unset, fallback to system preference.
- `matchMedia` unavailable (unlikely) -> fallback light.
- JS disabled -> light theme only, app remains functional, and floating toggle is not shown.
- Cookie write blocked/fails -> keep current-page visual switch only; persistence not guaranteed on next load.
- System theme changes after load:
- if cookie exists, keep explicit user preference,
- if cookie absent, follow resolved system preference on next navigation/load.
## Testing and Verification
Because no automated test suite is configured, verification is manual + syntax checks.
## Manual Functional Checks
- Open each in-scope view and verify toggle is visible at top-right:
- `/home`, vehicle details, `/settings`, `/login`, `/setup`.
- With no cookie set:
- verify first load follows system preference.
- Click toggle:
- verify immediate theme switch,
- verify cookie is written,
- verify choice persists across reload and cross-page navigation.
- Set an invalid cookie value manually:
- verify fallback to system preference.
- Open export page:
- verify always light and no toggle.
- Disable JS (or simulate no JS):
- verify app remains usable in light mode and toggle is not rendered.
- Simulate system-theme scenario with no cookie:
- verify resolved theme follows current system preference on load.
## Definition of Done
- `views/partials/theme.php` exists and is included in all in-scope views only.
- Floating top-right toggle appears on all in-scope views and is keyboard accessible.
- Theme resolution order is enforced: valid cookie first, otherwise system preference, otherwise light fallback.
- Clicking toggle updates theme immediately and attempts cookie persistence.
- Invalid cookie values are ignored safely.
- Export page remains always light and has no toggle.
- `php -l` passes for each changed PHP file.
## Syntax Checks
Run `php -l` for each changed PHP file:
- `views/partials/theme.php`
- each modified view file in scope
## Risks and Mitigations
- Risk: visual regressions from many color classes.
- Mitigation: limit edits to color semantics; keep structure/layout intact.
- Risk: flash of incorrect theme.
- Mitigation: apply theme class in early `<head>` script before body render.
- Risk: inconsistent behavior across pages.
- Mitigation: centralize logic in shared partial.
## Implementation Boundaries
- No refactors unrelated to theme support.
- Keep MVC boundaries untouched (view-only changes).
- Keep existing UX flows, redirects, and auth/setup guards unchanged.