Now I remember the theme
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-11
|
||||
62
openspec/changes/archive/2026-02-11-remember-theme/design.md
Normal file
62
openspec/changes/archive/2026-02-11-remember-theme/design.md
Normal file
@@ -0,0 +1,62 @@
|
||||
## Context
|
||||
|
||||
The site already supports theme selection via a floating notch and persists the user's choice using `localStorage` (key: `site.theme`). Theme selection affects the root document via `html[data-theme]` and is initialized before first paint via an inline head script.
|
||||
|
||||
Two gaps remain:
|
||||
- Returning users should reliably see their last-selected theme even in environments where `localStorage` is unavailable.
|
||||
- We need analytics to measure theme switcher usage and preferred themes.
|
||||
|
||||
The site uses Umami for analytics. Most interactions are tracked via `data-umami-event*` attributes, and runtime-only events use `window.umami.track(...)`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Persist theme selection across visits with a robust fallback: `localStorage` primary, client-side cookie fallback.
|
||||
- Apply the stored theme before first paint when possible.
|
||||
- Emit a deterministic Umami event when a user changes theme via the theme notch.
|
||||
|
||||
**Non-Goals:**
|
||||
- Server-side rendering of theme choice (the site is statically built; no request-time HTML variation).
|
||||
- Tracking theme selection on initial page load (only user-initiated changes).
|
||||
- Adding new UI beyond the existing theme notch.
|
||||
|
||||
## Decisions
|
||||
|
||||
1) Persistence mechanism and precedence
|
||||
|
||||
- **Decision**: Read theme preference in this order:
|
||||
1. `localStorage` (`site.theme`)
|
||||
2. Cookie (`site_theme`)
|
||||
3. Environment signals (forced-colors, prefers-color-scheme)
|
||||
- **Rationale**: `localStorage` is already in use and provides a stable primary store. Cookies provide a resilient fallback when storage access is blocked or throws.
|
||||
- **Alternatives considered**:
|
||||
- Cookie-only: simpler but unnecessary regression from existing behavior.
|
||||
- URL param: not persistent and adds user-visible noise.
|
||||
|
||||
2) Cookie format and attributes
|
||||
|
||||
- **Decision**: Store `site_theme=<theme>` with `Max-Age=31536000`, `Path=/`, `SameSite=Lax`. Set `Secure` when running under HTTPS.
|
||||
- **Rationale**: First-party cookie with long TTL provides continuity across visits. The cookie is readable from the inline head script for pre-paint initialization.
|
||||
|
||||
3) Analytics event shape
|
||||
|
||||
- **Decision**: Emit a custom Umami event via `window.umami.track("theme_switch", data)` only on user-initiated changes.
|
||||
- **Event properties**:
|
||||
- `target_id`: `theme.switch.<theme>`
|
||||
- `placement`: `theme_notch`
|
||||
- `theme`: new theme value (`dark` | `light` | `high-contrast`)
|
||||
- `prev_theme`: previous theme value (same enum) when known
|
||||
- **Rationale**: A dedicated event name makes reporting straightforward (no need to filter general `click`). Using `target_id`/`placement` keeps it compatible with the site's interaction taxonomy.
|
||||
- **Alternatives considered**:
|
||||
- Reuse `click` event: consistent, but mixes preference changes into general click reporting.
|
||||
|
||||
4) Avoid tracking initial theme restoration
|
||||
|
||||
- **Decision**: Do not emit `theme_switch` from the head theme-init script.
|
||||
- **Rationale**: We want to measure explicit user interaction with the notch, not implicit restoration.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Cookie and storage may both be blocked in restrictive environments → fallback to environment signals; no persistence.
|
||||
- Umami may be disabled/unconfigured or not loaded at event time → guard with `typeof window.umami !== "undefined"` and keep behavior non-fatal.
|
||||
- Using cookies introduces another persistence layer → must keep `localStorage` and cookie consistent on successful theme changes.
|
||||
@@ -0,0 +1,25 @@
|
||||
## Why
|
||||
|
||||
Theme choice is part of a user's identity and comfort on the site; returning visitors should land in the theme they previously selected. We also need measurement to understand whether the theme switcher is being used and which themes are preferred.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Persist the user's selected theme across visits so returning users see the last-selected theme immediately.
|
||||
- Add Umami tracking for theme selection changes so theme switch usage can be measured and segmented.
|
||||
- Improve robustness of persistence by supporting either localStorage or a client-side cookie (cookie fallback when localStorage is unavailable).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- (none)
|
||||
|
||||
### Modified Capabilities
|
||||
- `site-theming`: Extend theme persistence requirements to explicitly cover returning visits and define acceptable client-side persistence mechanisms / fallback behavior.
|
||||
- `analytics-umami`: Add a custom event emitted from client-side code for theme selection changes (using Umami's JS API when needed).
|
||||
- `interaction-tracking-taxonomy`: Define the theme selection event name and required event properties (at minimum `target_id` and `placement`, plus theme metadata).
|
||||
|
||||
## Impact
|
||||
|
||||
- Frontend: update theme switcher behavior in `site/src/layouts/BaseLayout.astro` (persistence/fallback and event emission).
|
||||
- Analytics: new Umami event(s) added; dashboards/filters can segment by selected theme and placement.
|
||||
- Specs: update the modified capabilities above to reflect the new requirements.
|
||||
@@ -0,0 +1,21 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Theme switch tracking event
|
||||
When Umami is enabled, the site MUST emit a custom event when the user changes theme via the theme switcher UI.
|
||||
|
||||
The site MUST emit the event using Umami's JavaScript API (`umami.track(...)`) so runtime properties can be included.
|
||||
|
||||
The event name MUST be `theme_switch`.
|
||||
|
||||
The emitted event MUST include, at minimum:
|
||||
- `target_id`
|
||||
- `placement`
|
||||
- `theme`
|
||||
|
||||
#### Scenario: Theme switch emits event
|
||||
- **WHEN** a user selects `high-contrast` in the theme switcher notch
|
||||
- **THEN** the site emits a `theme_switch` event with `theme=high-contrast` and a stable `target_id`
|
||||
|
||||
#### Scenario: Missing Umami does not break switching
|
||||
- **WHEN** Umami is not configured or the Umami script is not present
|
||||
- **THEN** theme switching and persistence still work and no browser error is thrown
|
||||
@@ -0,0 +1,23 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Theme switch event taxonomy
|
||||
The tracking taxonomy MUST define an event for theme switching.
|
||||
|
||||
The event name MUST be `theme_switch`.
|
||||
|
||||
The `theme_switch` event MUST include, at minimum:
|
||||
- `target_id`
|
||||
- `placement`
|
||||
- `theme`
|
||||
|
||||
The event SHOULD include `prev_theme` when available.
|
||||
|
||||
The taxonomy MUST define the `target_id` namespace for theme switching as:
|
||||
- `theme.switch.<theme>`
|
||||
|
||||
The taxonomy MUST define the `placement` value for the theme switcher notch as:
|
||||
- `theme_notch`
|
||||
|
||||
#### Scenario: Theme switch target_id is deterministic
|
||||
- **WHEN** a user selects `light` theme using the theme notch
|
||||
- **THEN** the event is emitted with `target_id=theme.switch.light` and `placement=theme_notch`
|
||||
@@ -0,0 +1,25 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Theme persistence works across visits with fallback
|
||||
The site MUST persist the user's theme selection across visits so returning users see the last-selected theme.
|
||||
|
||||
The site MUST use client-side persistence and MUST support a fallback mechanism:
|
||||
- Primary: `localStorage`
|
||||
- Fallback: a client-side cookie
|
||||
|
||||
The effective theme selection order MUST be:
|
||||
1) Stored theme in `localStorage` (if available)
|
||||
2) Stored theme in a cookie (if localStorage is unavailable)
|
||||
3) Default selection using environment signals
|
||||
|
||||
#### Scenario: LocalStorage persists across a later visit
|
||||
- **WHEN** a user selects `light` theme and later returns to the site in the same browser
|
||||
- **THEN** the site initializes in `light` theme before first paint
|
||||
|
||||
#### Scenario: Cookie fallback is used when localStorage is unavailable
|
||||
- **WHEN** the browser environment blocks `localStorage` access and the user selects `dark` theme
|
||||
- **THEN** the theme is persisted using a client-side cookie and is restored on the next visit
|
||||
|
||||
#### Scenario: No persistence available falls back to defaults
|
||||
- **WHEN** both `localStorage` and cookie persistence are unavailable
|
||||
- **THEN** the site falls back to default theme selection using environment signals
|
||||
17
openspec/changes/archive/2026-02-11-remember-theme/tasks.md
Normal file
17
openspec/changes/archive/2026-02-11-remember-theme/tasks.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## 1. Theme Persistence Across Visits
|
||||
|
||||
- [x] 1.1 Add cookie helpers in `site/src/layouts/BaseLayout.astro` to read/write a `site_theme` cookie (Path=/, SameSite=Lax, 1y TTL; Secure on HTTPS)
|
||||
- [x] 1.2 Update the head theme-init script in `site/src/layouts/BaseLayout.astro` to prefer localStorage, then cookie, then environment signals
|
||||
- [x] 1.3 Update the theme setter in `site/src/layouts/BaseLayout.astro` to keep localStorage and cookie in sync on user selection (cookie fallback when localStorage throws)
|
||||
|
||||
## 2. Umami Tracking For Theme Switch
|
||||
|
||||
- [x] 2.1 Define event emission on user-initiated theme changes using `window.umami.track("theme_switch", ...)` (guarded when Umami is missing)
|
||||
- [x] 2.2 Use taxonomy fields for the event payload: `target_id=theme.switch.<theme>`, `placement=theme_notch`, include `theme` and `prev_theme` when available
|
||||
- [x] 2.3 Ensure theme restoration on page load does NOT emit `theme_switch` (only explicit user interaction)
|
||||
|
||||
## 3. Verification
|
||||
|
||||
- [x] 3.1 Run `npm run build` and verify output includes cookie read fallback in the head init script
|
||||
- [x] 3.2 Run `npm test` and ensure no new failures are introduced
|
||||
- [x] 3.3 Manual: switch theme, reload, and confirm persistence; repeat with localStorage disabled to confirm cookie fallback
|
||||
Reference in New Issue
Block a user