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 24212ca649

24212ca6491bfebc44aad929e1ea84e8108cd8c4

parent: 12b2f378a9

Unsigned

cmc <hello@cleberg.net> · 2026-10-19 05:00 UTC

Redesign the guided tours sheet (!43)

Guided tours sheet in the app's style, like Events: title, accent bar and intro; one card per tour with its months, stop count, first four stops as the headline, intro and Start tour. This month's tour is marked and outlined in red. Tour titles shortened to "Sep–Nov evenings" etc. Blue tint removed.

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,