224 lines
8.2 KiB
Markdown
224 lines
8.2 KiB
Markdown
# 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.
|