nvrsion: promote trunk to dev
Nucleic-Promote: 1
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user