nvrsion: promote trunk to dev
Nucleic-Promote: 1
This commit is contained in:
@@ -19,16 +19,11 @@ struct HomeView: View {
|
|||||||
uniquingKeysWith: +)
|
uniquingKeysWith: +)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var streak: Int {
|
/// Current activity streak with streak freezes folded in — the run of consecutive active
|
||||||
let cal = Calendar.current
|
/// days, where a banked freeze (one earned per five days) bridges a missed day. Shared with
|
||||||
var day = cal.startOfDay(for: Date())
|
/// the Mac via `StreakState`, so both devices report the same streak and frozen days.
|
||||||
if (activityByDay[day] ?? 0) == 0 { day = cal.date(byAdding: .day, value: -1, to: day)! }
|
private var streakState: StreakState {
|
||||||
var count = 0
|
StreakState.compute(activityByDay: activityByDay)
|
||||||
while (activityByDay[day] ?? 0) > 0 {
|
|
||||||
count += 1
|
|
||||||
day = cal.date(byAdding: .day, value: -1, to: day)!
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var greeting: String {
|
private var greeting: String {
|
||||||
@@ -41,7 +36,10 @@ struct HomeView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some 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 {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 22) {
|
VStack(alignment: .leading, spacing: 22) {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
@@ -50,14 +48,14 @@ struct HomeView: View {
|
|||||||
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
|
Text(subtitle).font(.subheadline).foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
if streak > 0 { StreakBadge(days: streak) }
|
if streak.streak > 0 { StreakBadge(days: streak.streak, freezes: streak.availableFreezes) }
|
||||||
}
|
}
|
||||||
|
|
||||||
statCards
|
statCards
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("Activity").font(.headline)
|
Text("Activity").font(.headline)
|
||||||
ActivityGrid(activityByDay: activityByDay)
|
ActivityGrid(activityByDay: activityByDay, frozenDays: streak.frozenDays)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.card()
|
.card()
|
||||||
@@ -137,6 +135,8 @@ private struct StatCard: View {
|
|||||||
/// A lightning bolt whose charge grows with the streak (mirrors the Mac StreakBadge).
|
/// A lightning bolt whose charge grows with the streak (mirrors the Mac StreakBadge).
|
||||||
struct StreakBadge: View {
|
struct StreakBadge: View {
|
||||||
let days: Int
|
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) }
|
private var intensity: Double { min(1, Double(max(0, days - 1)) / 20) }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -154,6 +154,14 @@ struct StreakBadge: View {
|
|||||||
Text("\(days)").font(.headline.monospacedDigit())
|
Text("\(days)").font(.headline.monospacedDigit())
|
||||||
Text("day streak").font(.caption2).foregroundStyle(.secondary)
|
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)
|
.padding(.horizontal, 12).padding(.vertical, 8)
|
||||||
.background(Color(.secondarySystemBackground), in: Capsule())
|
.background(Color(.secondarySystemBackground), in: Capsule())
|
||||||
@@ -163,6 +171,9 @@ struct StreakBadge: View {
|
|||||||
/// GitHub-style activity grid: columns = weeks (old→new), rows = Sun…Sat.
|
/// GitHub-style activity grid: columns = weeks (old→new), rows = Sun…Sat.
|
||||||
struct ActivityGrid: View {
|
struct ActivityGrid: View {
|
||||||
let activityByDay: [Date: Int]
|
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 cell: CGFloat = 13
|
||||||
private let gap: CGFloat = 3
|
private let gap: CGFloat = 3
|
||||||
|
|
||||||
@@ -185,8 +196,12 @@ struct ActivityGrid: View {
|
|||||||
VStack(spacing: gap) {
|
VStack(spacing: gap) {
|
||||||
ForEach(0..<7, id: \.self) { row in
|
ForEach(0..<7, id: \.self) { row in
|
||||||
let date = cal.date(byAdding: .day, value: col * 7 + row, to: gridStart)!
|
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)
|
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)
|
.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 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 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
|
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
|
static let neutral = Color.secondary
|
||||||
/// The orchestra (orchestration mode) signature gold — mirrors the Mac's
|
/// The orchestra (orchestration mode) signature gold — mirrors the Mac's
|
||||||
/// `AppTheme.orchestra`. Used for the effort picker's Orchestra option and the
|
/// `AppTheme.orchestra`. Used for the effort picker's Orchestra option and the
|
||||||
|
|||||||
Reference in New Issue
Block a user