nvrsion: promote trunk to dev

Nucleic-Promote: 1
This commit is contained in:
Nucleic
2026-06-26 01:29:27 -07:00
parent cfa823bc13
commit 8db01ac703
2 changed files with 32 additions and 14 deletions
@@ -19,16 +19,11 @@ struct HomeView: View {
uniquingKeysWith: +)
}
private var streak: Int {
let cal = Calendar.current
var day = cal.startOfDay(for: Date())
if (activityByDay[day] ?? 0) == 0 { day = cal.date(byAdding: .day, value: -1, to: day)! }
var count = 0
while (activityByDay[day] ?? 0) > 0 {
count += 1
day = cal.date(byAdding: .day, value: -1, to: day)!
}
return count
/// Current activity streak with streak freezes folded in — the run of consecutive active
/// days, where a banked freeze (one earned per five days) bridges a missed day. Shared with
/// the Mac via `StreakState`, so both devices report the same streak and frozen days.
private var streakState: StreakState {
StreakState.compute(activityByDay: activityByDay)
}
private var greeting: String {
@@ -41,7 +36,10 @@ struct HomeView: View {
}
var body: some View {
NavigationStack {
// Computed once and threaded down so the badge and grid agree on the streak and which
// days a freeze bridged.
let streak = streakState
return NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 22) {
HStack(alignment: .top) {
@@ -50,14 +48,14 @@ struct HomeView: View {
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
}
Spacer()
if streak > 0 { StreakBadge(days: streak) }
if streak.streak > 0 { StreakBadge(days: streak.streak, freezes: streak.availableFreezes) }
}
statCards
VStack(alignment: .leading, spacing: 10) {
Text("Activity").font(.headline)
ActivityGrid(activityByDay: activityByDay)
ActivityGrid(activityByDay: activityByDay, frozenDays: streak.frozenDays)
}
.frame(maxWidth: .infinity, alignment: .leading)
.card()
@@ -137,6 +135,8 @@ private struct StatCard: View {
/// A lightning bolt whose charge grows with the streak (mirrors the Mac StreakBadge).
struct StreakBadge: View {
let days: Int
/// Streak freezes in reserve (one per five days), shown as a small snowflake tally.
var freezes: Int = 0
private var intensity: Double { min(1, Double(max(0, days - 1)) / 20) }
var body: some View {
@@ -154,6 +154,14 @@ struct StreakBadge: View {
Text("\(days)").font(.headline.monospacedDigit())
Text("day streak").font(.caption2).foregroundStyle(.secondary)
}
if freezes > 0 {
HStack(spacing: 2) {
Image(systemName: "snowflake")
Text("\(freezes)").monospacedDigit()
}
.font(.caption.weight(.semibold))
.foregroundStyle(Palette.frozen)
}
}
.padding(.horizontal, 12).padding(.vertical, 8)
.background(Color(.secondarySystemBackground), in: Capsule())
@@ -163,6 +171,9 @@ struct StreakBadge: View {
/// GitHub-style activity grid: columns = weeks (old→new), rows = Sun…Sat.
struct ActivityGrid: View {
let activityByDay: [Date: Int]
/// Missed days a streak freeze kept alive — drawn in the frozen blue rather than the
/// empty-day gray (mirrors the Mac grid).
var frozenDays: Set<Date> = []
private let cell: CGFloat = 13
private let gap: CGFloat = 3
@@ -185,8 +196,12 @@ struct ActivityGrid: View {
VStack(spacing: gap) {
ForEach(0..<7, id: \.self) { row in
let date = cal.date(byAdding: .day, value: col * 7 + row, to: gridStart)!
let isFuture = date > today
let isFrozen = !isFuture && frozenDays.contains(date)
RoundedRectangle(cornerRadius: 2)
.fill(date > today ? Color.clear : Palette.activity(level: Self.level(activityByDay[date] ?? 0)))
.fill(isFuture ? Color.clear
: isFrozen ? Palette.frozen
: Palette.activity(level: Self.level(activityByDay[date] ?? 0)))
.frame(width: cell, height: cell)
}
}
@@ -12,6 +12,9 @@ enum Palette {
static let success = Color(red: 0.30, green: 0.80, blue: 0.45) // green
static let paused = Color(red: 0.62, green: 0.49, blue: 0.93) // purple
static let danger = Color(red: 0.92, green: 0.34, blue: 0.34) // red
/// Cold "iced" blue for a streak-frozen day in the activity grid — mirrors the Mac's
/// `AppPalette.frozen`.
static let frozen = Color(red: 0.26, green: 0.64, blue: 0.96)
static let neutral = Color.secondary
/// The orchestra (orchestration mode) signature gold — mirrors the Mac's
/// `AppTheme.orchestra`. Used for the effort picker's Orchestra option and the