summaryrefslogtreecommitdiff
path: root/HutchWidgetExtension/NeedsAttentionWidget.swift
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-01 12:04:31 -0500
committerChristian Cleberg <[email protected]>2026-04-01 12:04:31 -0500
commit367a9d8e9592d0c604356e74edea97e0791b32cd (patch)
treed99bae033a8b962f67b51ceb11f8982b114f358b /HutchWidgetExtension/NeedsAttentionWidget.swift
parentc22287d5e256f5b23528114c5efec63069f9c2b5 (diff)
downloadhutch-367a9d8e9592d0c604356e74edea97e0791b32cd.tar.gz
hutch-367a9d8e9592d0c604356e74edea97e0791b32cd.tar.bz2
hutch-367a9d8e9592d0c604356e74edea97e0791b32cd.zip
fix: add large widget and apply UX fixes for all widget sizes
Implements: https://todo.sr.ht/~ccleberg/Hutch/8
Diffstat (limited to 'HutchWidgetExtension/NeedsAttentionWidget.swift')
-rw-r--r--HutchWidgetExtension/NeedsAttentionWidget.swift132
1 files changed, 106 insertions, 26 deletions
diff --git a/HutchWidgetExtension/NeedsAttentionWidget.swift b/HutchWidgetExtension/NeedsAttentionWidget.swift
index 0e177a7..6191d20 100644
--- a/HutchWidgetExtension/NeedsAttentionWidget.swift
+++ b/HutchWidgetExtension/NeedsAttentionWidget.swift
@@ -44,7 +44,7 @@ struct NeedsAttentionWidget: Widget {
}
.configurationDisplayName("Needs Attention")
.description("Unread inbox threads, assigned tickets, and failed builds.")
- .supportedFamilies([.systemSmall, .systemMedium])
+ .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
@@ -61,6 +61,8 @@ private struct NeedsAttentionWidgetView: View {
switch family {
case .systemMedium:
mediumView(snapshot: snapshot)
+ case .systemLarge:
+ largeView(snapshot: snapshot)
default:
smallView(snapshot: snapshot)
}
@@ -75,17 +77,6 @@ private struct NeedsAttentionWidgetView: View {
}
}
- private var header: some View {
- VStack(alignment: .leading, spacing: 1) {
- Text("Hutch")
- .font(.caption.weight(.semibold))
- .foregroundStyle(.secondary)
- Text("Needs Attention")
- .font(.headline)
- .lineLimit(1)
- }
- }
-
private func smallView(snapshot: NeedsAttentionSnapshot) -> some View {
VStack(alignment: .leading, spacing: 12) {
Spacer(minLength: 0)
@@ -108,9 +99,7 @@ private struct NeedsAttentionWidgetView: View {
}
private func mediumView(snapshot: NeedsAttentionSnapshot) -> some View {
- VStack(alignment: .leading, spacing: 18) {
- header
- .padding(.top, 6)
+ VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top, spacing: 12) {
metricColumn(
count: snapshot.unreadInboxThreads,
@@ -137,23 +126,47 @@ private struct NeedsAttentionWidgetView: View {
.padding(.bottom, 16)
}
- private var clearState: some View {
- VStack(alignment: .leading, spacing: 12) {
- header
+ private func largeView(snapshot: NeedsAttentionSnapshot) -> some View {
+ VStack(alignment: .leading, spacing: 18) {
+ metricRow(
+ count: snapshot.unreadInboxThreads,
+ label: "Unread threads",
+ systemImage: "tray",
+ tint: unreadTint(for: snapshot.unreadInboxThreads)
+ )
+ metricRow(
+ count: snapshot.assignedOpenTickets,
+ label: "Assigned tickets",
+ systemImage: "ticket"
+ )
+ metricRow(
+ count: snapshot.failedBuilds,
+ label: "Failed builds",
+ systemImage: "hammer",
+ tint: failedTint(for: snapshot.failedBuilds)
+ )
Spacer(minLength: 0)
- Text("All clear")
- .font(.title3.weight(.semibold))
- Text("No unread threads, assigned tickets, or failed builds.")
- .font(.caption)
- .foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
}
- .padding()
+ .padding(20)
+ }
+
+ private var clearState: some View {
+ Group {
+ switch family {
+ case .systemSmall:
+ smallClearState
+ case .systemMedium:
+ mediumClearState
+ case .systemLarge:
+ largeClearState
+ default:
+ smallClearState
+ }
+ }
}
private var fallbackState: some View {
VStack(alignment: .leading, spacing: 12) {
- header
Spacer(minLength: 0)
Text("Open Hutch")
.font(.title3.weight(.semibold))
@@ -202,6 +215,73 @@ private struct NeedsAttentionWidgetView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
+ private func metricRow(count: Int?, label: String, systemImage: String, tint: Color = .primary) -> some View {
+ HStack(alignment: .center, spacing: 12) {
+ Image(systemName: systemImage)
+ .font(.title3.weight(.semibold))
+ .foregroundStyle(tint)
+ .frame(width: 24, alignment: .leading)
+ Text(displayCount(count))
+ .font(.system(size: 30, weight: .semibold, design: .rounded))
+ .monospacedDigit()
+ .foregroundStyle(tint)
+ Text(label)
+ .font(.body)
+ .foregroundStyle(.secondary)
+ Spacer(minLength: 0)
+ }
+ }
+
+ private var smallClearState: some View {
+ VStack(spacing: 6) {
+ Spacer(minLength: 0)
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 52, weight: .regular))
+ .foregroundStyle(clearStateTint)
+ Text("0 items")
+ .font(.caption2.weight(.semibold))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ Spacer(minLength: 0)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .padding()
+ }
+
+ private var mediumClearState: some View {
+ VStack(spacing: 10) {
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 26, weight: .regular))
+ .foregroundStyle(clearStateTint)
+ Text("Nothing to review")
+ .font(.body.weight(.semibold))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ .minimumScaleFactor(0.9)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
+ .padding()
+ }
+
+ private var largeClearState: some View {
+ VStack(spacing: 12) {
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 38, weight: .regular))
+ .foregroundStyle(clearStateTint)
+ Text("Nothing to review")
+ .font(.title3.weight(.semibold))
+ .foregroundStyle(.primary)
+ .lineLimit(1)
+ .minimumScaleFactor(0.9)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
+ .padding()
+ }
+
+ private var clearStateTint: Color {
+ Color(.systemGreen).opacity(0.75)
+ }
+
private func displayCount(_ count: Int?) -> String {
guard let count else { return "—" }
return "\(count)"