diff options
Diffstat (limited to 'docs/superpowers/specs/2026-03-23-solstice-widget-design.md')
| -rw-r--r-- | docs/superpowers/specs/2026-03-23-solstice-widget-design.md | 307 |
1 files changed, 0 insertions, 307 deletions
diff --git a/docs/superpowers/specs/2026-03-23-solstice-widget-design.md b/docs/superpowers/specs/2026-03-23-solstice-widget-design.md deleted file mode 100644 index d91f1f2..0000000 --- a/docs/superpowers/specs/2026-03-23-solstice-widget-design.md +++ /dev/null @@ -1,307 +0,0 @@ -# Solstice Countdown Widget & App Design - -**Date:** 2026-03-23 -**Project:** Solverv (Solstice Countdown) -**Status:** Design Approved - ---- - -## Overview - -Build an iOS app that displays a countdown to the next solstice or equinox, with season-specific imagery and detailed information. The app includes: -- **Widget Extension:** Compact countdown widget in small, medium, and large sizes -- **Main App:** Info screen showing expanded details including sunrise/sunset times and upcoming events - ---- - -## Requirements - -### Functional Requirements -1. Track all four annual solstices/equinoxes (Spring, Summer, Autumn, Winter) -2. Display countdown to next event in **days only** -3. Show season-specific images that change based on upcoming event -4. Calculate and display sunrise/sunset times for user's current location -5. Show expanded event details in main app (exact times, season descriptions, upcoming event previews) -6. Support multiple widget sizes (small, medium, large) - -### Non-Functional Requirements -- All calculations happen locally (no external APIs) -- Works offline after initial setup -- Respects location privacy with explicit permission -- Single location permission prompt (shared via AppGroup) -- Data synced between app and widget via AppGroup container - ---- - -## Architecture - -### Data Models - -**SolsticeEvent** -``` -- name: String (e.g., "Summer Solstice", "Spring Equinox") -- date: Date (UTC) -- season: Season (enum: spring, summer, autumn, winter) -- seasonDescription: String (brief description of the season) -``` - -**SolsticeData** -``` -- events: [SolsticeEvent] (hardcoded data, 2025-2030) -- nextEvent() -> SolsticeEvent (returns next upcoming event) -- allUpcoming(count: Int) -> [SolsticeEvent] (returns next N events) -- daysUntil(_ event: SolsticeEvent) -> Int -- progressToEvent(_ event: SolsticeEvent) -> (elapsed: Int, total: Int) -``` - -**SunTimes** -``` -- latitude: Double -- longitude: Double -- date: Date -- sunrise() -> Date -- sunset() -> Date -``` - -### Data Sharing - -**AppGroup Container:** Store user location and cached sunrise/sunset times using the container ID `group.com.ivarlovlie.solverv` - -**AppGroup Data Schema:** -Stored in UserDefaults using the `group.com.ivarlovlie.solverv` container. -```json -{ - "userLocation.latitude": Double (e.g., 59.9139), - "userLocation.longitude": Double (e.g., 10.7522), - "userLocation.timestamp": String (ISO 8601, e.g., "2026-03-23T10:30:00Z"), - "userLocation.isDefaultLocation": Boolean (true = Greenwich, false = user-granted location), - - "sunTimes.date": String (ISO 8601 date only, e.g., "2026-03-23"), - "sunTimes.sunrise": String (ISO 8601 datetime in local timezone, e.g., "2026-03-23T06:42:00"), - "sunTimes.sunset": String (ISO 8601 datetime in local timezone, e.g., "2026-03-23T18:15:00"), - "sunTimes.timestamp": String (ISO 8601, when calculated, e.g., "2026-03-23T10:30:00Z") -} -``` -All Date values stored as ISO 8601 strings for cross-process compatibility. - -- **Widget Timeline:** Updates at midnight (local time) using `timelineReloadPolicy: .after(nextMidnight)` -- Both app and widget read from the same AppGroup container for consistency -- Cache invalidation: Sunrise/sunset cache is recalculated if stored date differs from today or location changes - ---- - -## Widget Specification - -### Small Widget (169×169) -- **Layout:** Vertical stack - - Seasonal image (fills most of space) - - Event name (small caption) - - Countdown in days (large, bold text) -- **Refresh:** `timelineReloadPolicy: .after(nextMidnight)` where `nextMidnight` is calculated as: - ```swift - var calendar = Calendar.current - var components = calendar.dateComponents([.year, .month, .day], from: Date()) - components.hour = 0 - components.minute = 0 - components.second = 0 - let todayMidnight = calendar.date(from: components)! - let nextMidnight = calendar.date(byAdding: .day, value: 1, to: todayMidnight)! - ``` - The widget refreshes daily at midnight local time, automatically accounting for DST transitions. -- **Purpose:** Quick glance at how many days remain - -### Medium Widget (364×169) -- **Layout:** Horizontal split - - Left: Seasonal image (square) - - Right: Vertical stack with event name, countdown (large), progress bar -- **Refresh:** `timelineReloadPolicy: .after(nextMidnight)` — refreshes at midnight local time to update countdown -- **Purpose:** Balance of visual and numeric information - -### Large Widget (364×364) -- **Layout:** Vertical stack - - Top half: Seasonal image - - Bottom half: Event name, countdown, progress bar, preview of next 3 upcoming events (mini list) -- **Refresh:** `timelineReloadPolicy: .after(nextMidnight)` — refreshes at midnight local time to update countdown -- **Purpose:** Comprehensive view with upcoming events preview - -### Images -- One image per season (spring, summer, autumn, winter) -- Sourced from Assets.xcassets -- Same image shown for all events in that season - -**Image Specifications:** -- **Asset Names:** `SeasonSpring`, `SeasonSummer`, `SeasonAutumn`, `SeasonWinter` (stored in Assets.xcassets) -- **Color Set Strategy:** Each season asset has two variants (light and dark mode) using Xcode's Color Set appearance settings -- **Aspect Ratio:** 1:1 (square) -- **Resolutions per variant:** - - 1x: 1024×1024 - - 2x: 2048×2048 - - 3x: 3072×3072 -- **Safe Area:** Ensure important visual content avoids outer 20-point margin (on a 1024×1024 base, keep content within 960×960 center area) -- **Format:** PNG with alpha channel -- **Fallback:** If image fails to load, display solid color matching the season (spring: #4CAF50, summer: #FFC107, autumn: #FF9800, winter: #2196F3) - ---- - -## Main App Info Screen - -### Top Section -- Seasonal image (landscape orientation friendly) -- Next event name (large) -- Countdown in days (very large, prominent) -- Progress bar showing days elapsed since the previous solstice/equinox divided by total days until next solstice/equinox - - Calculation: `(today - lastSolsticeDate) / (nextSolsticeDate - lastSolsticeDate)` - - Example: Winter Solstice was Dec 21, 2025 (passed). Spring Equinox is Mar 20, 2026 (89 days later). On Jan 21, 2026, progress is 31/89 days elapsed. - - The progress bar **resets to 0%** the moment a new solstice/equinox occurs - -### Middle Section -- **Today's Sun Times** - - Sunrise time - - Sunset time - - Calculated from device location -- **Season Info** - - Season name - - Brief description (e.g., "Spring Equinox — Day and night are approximately equal length") - -### Bottom Section -- **Upcoming Events List** (scrollable) - - Shows next 8-12 events - - Each row displays: - - Event name (e.g., "Spring Equinox") - - Date/Time in local timezone, 12-hour format with AM/PM (e.g., "Mar 20, 2026 2:46 PM") - - Days remaining as integer (e.g., "45 days") - - Season color indicator: 12pt circle matching the season's primary color (spring: green, summer: yellow, autumn: orange, winter: blue) - -### Navigation -- Tab bar or simple navigation to this screen -- Refresh button to manually update sunrise/sunset (in case location changed) - ---- - -## Time Zone & Location Handling - -### Location -- Request permission on first app launch -- Store latitude/longitude in AppGroup container -- Fall back to Greenwich/UTC (0°, 0°) if permission denied -- User can manually update location in app settings - -### Time Zones -- All solstice times stored in UTC (as they are now) -- Convert to user's local timezone for display -- Sunrise/sunset calculated for user's timezone and location - -### Sunrise/Sunset Algorithm -- Implement a simplified solar position algorithm based on the NOAA algorithm (https://github.com/NOAA-OWP/sunpy) -- No external APIs; algorithm is self-contained -- **Algorithm Reference:** NOAA Solar Position Algorithm - - Calculate solar declination using Spencer's formula - - Calculate equation of time for date - - Calculate hour angle at sunrise/sunset - - Convert to local solar time then UTC -- **Inputs:** latitude (Double), longitude (Double), date (Date in user's timezone) -- **Outputs:** sunrise (Date), sunset (Date) in local timezone -- **Cache:** Store in AppGroup container; recalculate daily or when location changes -- **Accuracy:** Results valid to ±2 minutes - ---- - -## Error Handling & Edge Cases - -### Location Permission -- App requests permission on first launch -- If denied, use default location (Greenwich) and notify user -- User can grant permission later in system Settings - -### Time Zone Edge Cases -- Solstice at midnight: Display correctly in both UTC and local time -- User crosses timezone: Times update automatically on app launch -- Widget timezone: Uses device timezone (set by system) - -### Data Integrity -- Solstice dates are hardcoded and immutable -- Sunrise/sunset cached but recalculated daily -- Widget syncs with app via AppGroup on launch - ---- - -## Testing Strategy - -### Widget Testing -- Preview all three widget sizes with mock solstice data -- Verify countdown updates correctly across timezone boundaries -- Test image display in different iOS versions (iOS 17 fallback) - -### App Testing -- Verify sunrise/sunset calculations against known values -- Test location permission flows (allowed, denied, not yet asked) -- Test data sync between app and widget -- Verify time zone conversions for various user locations - -### Integration Testing -- Widget refreshes daily and displays current countdown -- App and widget show consistent "next event" data -- Location changes update sunrise/sunset in real time - ---- - -## Implementation Notes - -### Existing Code -- `SolvervDef` already contains solstice dates (2025-2030) -- Widget structure (`Solsnu_Widget.swift`) is scaffolded -- Main app has basic SwiftUI structure ready for info screen - -### New Components to Build -- `SunTimes` calculation (sunrise/sunset) -- `SolsticeEvent` model -- Widget layout variants (small, medium, large) -- Info screen UI -- AppGroup data sharing - -### Dependencies -- None (all calculations are built-in or custom) -- WidgetKit (already available) -- SwiftUI (already used) - ---- - -## Additional Clarifications - -### Solstice Date Coverage -- Hardcoded data spans 2025–2030 -- After 2030, app continues functioning but upcoming events won't display beyond December 2030 -- Plan for data expansion before 2030: extend to 2040+ in an app update - -### Season Assignment -- The displayed season for an event is always the season being celebrated -- Example: Spring Equinox always displays spring imagery (green), even if shown during late winter on the calendar - -### Offline Functionality -- **First Launch:** - - App requests location permission - - If granted: Fetch location and calculate sunrise/sunset for that location, cache in AppGroup container - - If denied: Use default location (Greenwich, 0°/0°) and cache as `isDefaultLocation: true` - - All subsequent uses work offline -- **Subsequent Use:** - - App and widget work entirely offline; solstice dates and countdown calculations are all local - - Sunrise/sunset times use cached values from AppGroup container -- **State Recovery on Permission Changes:** - - If app is running when user changes location permission in Settings, recalculate sunrise/sunset on next app foreground event - - If AppGroup container is missing or corrupted on launch, fall back to Greenwich location and log error -- **Widget Behavior When Location Unavailable:** - - Widget always displays countdown (never fails) - - Sunrise/sunset times display as "—" if location permission denied and no cached data exists - ---- - -## Success Criteria - -✅ Widget displays countdown in days -✅ Seasonal image changes based on next event -✅ All four solstices/equinoxes tracked -✅ Sunrise/sunset times calculated from location -✅ Info screen shows all requested details -✅ App and widget data stay in sync -✅ Works offline after initial setup -✅ All three widget sizes render correctly |
