Now I remember the theme
Some checks failed
ci / site (push) Has been cancelled
publish-image / publish (push) Has been cancelled

This commit is contained in:
2026-02-10 20:38:38 -05:00
parent 70710239c7
commit f50a828535
19 changed files with 321 additions and 304 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-11

View 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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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`

View File

@@ -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

View 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