krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

Commit d8b3f37247

d8b3f3724778ab7a2138104496aa9ad29f490618

parent: 12b2f378a9

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-19 04:59 UTC

Redesign the guided tours sheet

Layout: unified · split

App/Sources/SkyScreen.swift +1 −1
@@ -94,7 +94,7 @@ struct SkyScreen: View {
94 .preferredColorScheme(.dark) 94 .preferredColorScheme(.dark)
95 } 95 }
96 .sheet(isPresented: $showTours) { 96 .sheet(isPresented: $showTours) {
97 TourPicker { tour = $0; tourStep = 0; aim(at: $0.steps[0].target) }.preferredColorScheme(.dark).tint(.blue) 97 TourPicker { tour = $0; tourStep = 0; aim(at: $0.steps[0].target) }.preferredColorScheme(.dark)
98 } 98 }
99 .onChange(of: tourStep) { if let tour { aim(at: tour.steps[tourStep].target) } } 99 .onChange(of: tourStep) { if let tour { aim(at: tour.steps[tourStep].target) } }
100 .sheet(isPresented: $showYear) { 100 .sheet(isPresented: $showYear) {
App/Sources/TourViews.swift +56 −14
@@ -6,26 +6,68 @@ struct TourPicker: View {
6 var onPick: (Tour) -> Void 6 var onPick: (Tour) -> Void
7 @Environment(\.dismiss) private var dismiss 7 @Environment(\.dismiss) private var dismiss
8 8
9 private let ink = Color(red: 0.95, green: 0.95, blue: 0.93)
10 private let accent = Color(red: 0.88, green: 0.24, blue: 0.19)
11 private let month = Calendar.current.component(.month, from: .now)
12
9 var body: some View { 13 var body: some View {
10 NavigationStack { 14 NavigationStack {
11 List(Tour.ordered(month: Calendar.current.component(.month, from: .now))) { tour in 15 ScrollView {
12 Button { 16 VStack(alignment: .leading, spacing: 0) {
13 onPick(tour) 17 Text("Guided tours").font(.title.weight(.medium))
14 dismiss() 18 Rectangle().fill(accent).frame(width: 48, height: 5).padding(.top, 8).padding(.bottom, 12)
15 } label: { 19 Text("A walk around the evening sky, one sight at a time. The view turns to each stop and marks what to look for. Best outside after dark, but they work anywhere.")
16 VStack(alignment: .leading, spacing: 4) { 20 .font(.subheadline).foregroundStyle(ink.opacity(0.75))
17 Text(tour.title).font(.body.weight(.medium)) 21 .padding(.bottom, 20)
18 Text(tour.intro).font(.footnote).foregroundStyle(.secondary) 22 ForEach(Tour.ordered(month: month)) { tour in card(tour) }
19 Text("\(tour.steps.count) stops").font(.caption).foregroundStyle(.secondary)
20 }
21 .padding(.vertical, 4)
22 } 23 }
23 .buttonStyle(.plain) 24 .padding(20)
25 .readableWidth()
24 } 26 }
25 .navigationTitle("Guided tours") 27 .background(Color.black.ignoresSafeArea())
26 .navigationBarTitleDisplayMode(.inline)
27 .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } 28 .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } }
28 } 29 }
30 .foregroundStyle(ink)
31 .tint(accent)
32 }
33
34 private func card(_ tour: Tour) -> some View {
35 let now = tour.months.contains(month)
36 return Button {
37 onPick(tour)
38 dismiss()
39 } label: {
40 VStack(alignment: .leading, spacing: 10) {
41 HStack {
42 if now {
43 Text("THIS MONTH").font(.caption2.weight(.bold)).tracking(0.8)
44 .padding(.horizontal, 6).padding(.vertical, 2)
45 .background(accent, in: RoundedRectangle(cornerRadius: 3))
46 }
47 Text(tour.title).font(.caption.weight(.medium)).foregroundStyle(ink.opacity(0.7))
48 Spacer()
49 Text("\(tour.steps.count) stops").font(.caption).foregroundStyle(ink.opacity(0.6))
50 }
51 Text(tour.steps.prefix(4).map(\.title).joined(separator: " · ") + (tour.steps.count > 4 ? " …" : ""))
52 .font(.title3.weight(.medium))
53 .multilineTextAlignment(.leading)
54 Text(tour.intro).font(.footnote).foregroundStyle(ink.opacity(0.75)).multilineTextAlignment(.leading)
55 HStack(spacing: 6) {
56 Text("Start tour").font(.subheadline.weight(.semibold))
57 Image(systemName: "arrow.right").font(.footnote.weight(.semibold))
58 }
59 .foregroundStyle(now ? accent : ink.opacity(0.8))
60 .padding(.top, 2)
61 }
62 .padding(18)
63 .frame(maxWidth: .infinity, alignment: .leading)
64 .background(ink.opacity(now ? 0.09 : 0.05), in: RoundedRectangle(cornerRadius: 18))
65 .overlay(RoundedRectangle(cornerRadius: 18).stroke(now ? accent.opacity(0.6) : ink.opacity(0.1), lineWidth: now ? 1 : 0.5))
66 .contentShape(RoundedRectangle(cornerRadius: 18))
67 }
68 .buttonStyle(.plain)
69 .padding(.bottom, 14)
70 .accessibilityHint("Starts the tour")
29 } 71 }
30} 72}
31 73
Packages/MoonriseKit/Sources/Library/Resources/tours/tours.json +4 −4
@@ -1,7 +1,7 @@
1[ 1[
2 { 2 {
3 "id": "winter", 3 "id": "winter",
4 "title": "December to February evenings", 4 "title": "Dec–Feb evenings",
5 "months": [ 5 "months": [
6 12, 6 12,
7 1, 7 1,
@@ -70,7 +70,7 @@
70 }, 70 },
71 { 71 {
72 "id": "spring", 72 "id": "spring",
73 "title": "March to May evenings", 73 "title": "Mar–May evenings",
74 "months": [ 74 "months": [
75 3, 75 3,
76 4, 76 4,
@@ -117,7 +117,7 @@
117 }, 117 },
118 { 118 {
119 "id": "summer", 119 "id": "summer",
120 "title": "June to August evenings", 120 "title": "Jun–Aug evenings",
121 "months": [ 121 "months": [
122 6, 122 6,
123 7, 123 7,
@@ -171,7 +171,7 @@
171 }, 171 },
172 { 172 {
173 "id": "autumn", 173 "id": "autumn",
174 "title": "September to November evenings", 174 "title": "Sep–Nov evenings",
175 "months": [ 175 "months": [
176 9, 176 9,
177 10, 177 10,