Add MoonWatch app, widget extension, shared module, and .gitignore

Adds the real MoonWatch implementation on top of the initial Xcode
template: new SwiftUI views (MoonDiskView, StarFieldView), the
MoonWatchWidget extension, and a Shared module containing the moon
phase calculator, shadow shape, and shared asset catalog. Also adds a
Swift/Xcode/macOS .gitignore and untracks per-user xcuserdata files.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-07 21:05:16 -07:00
parent 1698e3ce15
commit 87f8c7173c
17 changed files with 1211 additions and 24 deletions

View File

@@ -0,0 +1,97 @@
//
// MoonDiskView.swift
// MoonWatch
//
import SwiftUI
private struct MoonSouthernHemisphereKey: EnvironmentKey {
static let defaultValue = false
}
extension EnvironmentValues {
/// When true, waxing/waning orientation matches southern-sky convention (mirrored).
var moonSouthernHemisphere: Bool {
get { self[MoonSouthernHemisphereKey.self] }
set { self[MoonSouthernHemisphereKey.self] = newValue }
}
}
/// In-app moon view: photographic disk + atmospheric halo, drop shadows,
/// and limb stroke. The image+shadow rendering lives in `MoonImageDisk`
/// (in `Shared/`) so the widget can reuse it.
struct MoonDiskView: View {
/// Synodic phase in `[0, 1)`; 0 = new, 0.5 = full.
var normalizedPhase: Double
@Environment(\.moonSouthernHemisphere) private var southernHemisphere
var body: some View {
GeometryReader { geo in
let side = min(geo.size.width, geo.size.height)
ZStack {
Circle()
.fill(
RadialGradient(
colors: [
Color(red: 0.98, green: 0.94, blue: 0.78).opacity(0.32),
Color(red: 0.55, green: 0.50, blue: 0.85).opacity(0.12),
Color.clear,
],
center: .center,
startRadius: side * 0.32,
endRadius: side * 0.95
)
)
.frame(width: side * 1.9, height: side * 1.9)
.blendMode(.screen)
.blur(radius: side * 0.05)
MoonImageDisk(
normalizedPhase: normalizedPhase,
southernHemisphere: southernHemisphere
)
.frame(width: side, height: side)
}
.shadow(color: Color(red: 0.98, green: 0.94, blue: 0.78).opacity(0.30),
radius: side * 0.10)
.shadow(color: Color(red: 0.45, green: 0.55, blue: 0.95).opacity(0.30),
radius: side * 0.22)
.frame(width: side, height: side)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.aspectRatio(1, contentMode: .fit)
}
}
#Preview("Waxing Crescent") {
ZStack {
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
MoonDiskView(normalizedPhase: 0.12)
.frame(width: 280, height: 280)
}
}
#Preview("First Quarter") {
ZStack {
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
MoonDiskView(normalizedPhase: 0.25)
.frame(width: 280, height: 280)
}
}
#Preview("Full") {
ZStack {
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
MoonDiskView(normalizedPhase: 0.50)
.frame(width: 280, height: 280)
}
}
#Preview("Waning Gibbous") {
ZStack {
Color(red: 0.04, green: 0.05, blue: 0.12).ignoresSafeArea()
MoonDiskView(normalizedPhase: 0.62)
.frame(width: 280, height: 280)
}
}