Skip to content

Commit bc4ffd3

Browse files
authored
desktop: vertically center dashboard Tasks/Goals rows (#6906)
## Summary Follow-up to #6903. With the cards now equalized to the taller intrinsic height, the shorter card's rows pinned to the top, leaving a visible gap below. This PR centers the rows vertically inside each card so they float to the middle when the sibling card is taller. Wraps the existing row VStack in `VStack(spacing: 0) { Spacer(minLength: 0); … ; Spacer(minLength: 0) }.frame(maxHeight: .infinity)` for both empty and non-empty branches in `TasksWidget` and `GoalsWidget`. ## Test plan - [ ] Dashboard with 2 tasks and 4 goals — Tasks rows centered vertically; Goals fills. - [ ] Dashboard with 4 tasks and 1 goal — Goals row centered; Tasks fills. - [ ] Empty state on either side — empty content stays centered. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 496a03f + 073b179 commit bc4ffd3

3 files changed

Lines changed: 99 additions & 68 deletions

File tree

desktop/CHANGELOG.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
2-
"unreleased": [],
2+
"unreleased": [
3+
"Center dashboard Tasks/Goals rows vertically when one card is shorter than the other"
4+
],
35
"releases": [
46
{
57
"version": "0.11.345",

desktop/Desktop/Sources/MainWindow/Components/GoalsWidget.swift

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -38,45 +38,59 @@ struct GoalsWidget: View {
3838

3939
if goals.isEmpty {
4040
// Empty state — header already has a + button, so just offer
41-
// the AI generation action below.
42-
Button(action: { triggerGoalGeneration() }) {
43-
HStack(spacing: 6) {
44-
if isGeneratingGoal {
45-
ProgressView()
46-
.scaleEffect(0.6)
47-
.frame(width: 12, height: 12)
48-
} else {
49-
Image(systemName: "sparkles")
50-
.scaledFont(size: 12)
41+
// the AI generation action centered in the empty area.
42+
VStack(spacing: 0) {
43+
Spacer(minLength: 0)
44+
45+
Button(action: { triggerGoalGeneration() }) {
46+
HStack(spacing: 6) {
47+
if isGeneratingGoal {
48+
ProgressView()
49+
.scaleEffect(0.6)
50+
.frame(width: 12, height: 12)
51+
} else {
52+
Image(systemName: "sparkles")
53+
.scaledFont(size: 12)
54+
}
55+
Text(isGeneratingGoal ? "Generating..." : "Generate AI Goal")
56+
.scaledFont(size: 13, weight: .medium)
5157
}
52-
Text(isGeneratingGoal ? "Generating..." : "Generate AI Goal")
53-
.scaledFont(size: 13, weight: .medium)
58+
.foregroundColor(OmiColors.purplePrimary)
59+
.padding(.horizontal, 14)
60+
.padding(.vertical, 9)
61+
.omiControlSurface(fill: OmiColors.purplePrimary.opacity(0.12), radius: OmiChrome.chipRadius)
5462
}
55-
.foregroundColor(OmiColors.purplePrimary)
56-
.padding(.horizontal, 14)
57-
.padding(.vertical, 9)
58-
.omiControlSurface(fill: OmiColors.purplePrimary.opacity(0.12), radius: OmiChrome.chipRadius)
63+
.buttonStyle(.plain)
64+
.disabled(isGeneratingGoal)
65+
66+
Spacer(minLength: 0)
5967
}
60-
.buttonStyle(.plain)
61-
.disabled(isGeneratingGoal)
62-
.frame(maxWidth: .infinity)
63-
.padding(.vertical, 12)
68+
.frame(maxWidth: .infinity, maxHeight: .infinity)
6469
} else {
65-
// Goals list
66-
VStack(spacing: 14) {
67-
ForEach(Array(goals.enumerated()), id: \.element.id) { index, goal in
68-
GoalRowView(
69-
goal: goal,
70-
index: index,
71-
onTap: { editingGoal = goal },
72-
onUpdateProgress: { value in onUpdateProgress(goal, value) },
73-
onDelete: { onDeleteGoal(goal) },
74-
onGetInsight: {
75-
selectedGoalForInsight = goal
76-
}
77-
)
70+
// Goals list — centered vertically in remaining cell height
71+
// so a shorter Goals list floats to the middle when the
72+
// Tasks card determines the row's intrinsic height.
73+
VStack(spacing: 0) {
74+
Spacer(minLength: 0)
75+
76+
VStack(spacing: 14) {
77+
ForEach(Array(goals.enumerated()), id: \.element.id) { index, goal in
78+
GoalRowView(
79+
goal: goal,
80+
index: index,
81+
onTap: { editingGoal = goal },
82+
onUpdateProgress: { value in onUpdateProgress(goal, value) },
83+
onDelete: { onDeleteGoal(goal) },
84+
onGetInsight: {
85+
selectedGoalForInsight = goal
86+
}
87+
)
88+
}
7889
}
90+
91+
Spacer(minLength: 0)
7992
}
93+
.frame(maxWidth: .infinity, maxHeight: .infinity)
8094
}
8195
}
8296
.padding(22)

desktop/Desktop/Sources/MainWindow/Components/TodaysTasksWidget.swift

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,48 +30,63 @@ struct TasksWidget: View {
3030
}
3131

3232
if totalTaskCount == 0 {
33-
// Empty state
34-
VStack(spacing: 8) {
35-
Image(systemName: "checkmark.circle")
36-
.scaledFont(size: 28)
37-
.foregroundColor(OmiColors.textQuaternary)
38-
Text("No incomplete tasks")
39-
.scaledFont(size: 13)
40-
.foregroundColor(OmiColors.textTertiary)
33+
// Empty state — vertically centered in the cell
34+
VStack(spacing: 0) {
35+
Spacer(minLength: 0)
36+
37+
VStack(spacing: 8) {
38+
Image(systemName: "checkmark.circle")
39+
.scaledFont(size: 28)
40+
.foregroundColor(OmiColors.textQuaternary)
41+
Text("No incomplete tasks")
42+
.scaledFont(size: 13)
43+
.foregroundColor(OmiColors.textTertiary)
44+
}
45+
46+
Spacer(minLength: 0)
4147
}
42-
.frame(maxWidth: .infinity)
43-
.padding(.vertical, 12)
48+
.frame(maxWidth: .infinity, maxHeight: .infinity)
4449
} else {
4550
let allTasks = (combinedTodayTasks + recentTasks).prefix(3)
4651

47-
VStack(spacing: 10) {
48-
ForEach(Array(allTasks)) { task in
49-
TaskRowView(
50-
task: task,
51-
onToggle: { onToggleCompletion(task) }
52-
)
52+
// Task rows + "View all" centered vertically in remaining
53+
// cell height — when the Goals card is taller, the row
54+
// group floats to the middle instead of pinning to the top.
55+
VStack(spacing: 0) {
56+
Spacer(minLength: 0)
57+
58+
VStack(spacing: 10) {
59+
ForEach(Array(allTasks)) { task in
60+
TaskRowView(
61+
task: task,
62+
onToggle: { onToggleCompletion(task) }
63+
)
64+
}
5365
}
54-
}
5566

56-
Button(action: {
57-
NotificationCenter.default.post(
58-
name: .navigateToTasks,
59-
object: nil
60-
)
61-
}) {
62-
HStack {
63-
Spacer()
64-
Text("View all tasks")
65-
.scaledFont(size: 12, weight: .semibold)
66-
.foregroundColor(OmiColors.textSecondary)
67-
Image(systemName: "chevron.right")
68-
.scaledFont(size: 10)
69-
.foregroundColor(OmiColors.textSecondary)
70-
Spacer()
67+
Button(action: {
68+
NotificationCenter.default.post(
69+
name: .navigateToTasks,
70+
object: nil
71+
)
72+
}) {
73+
HStack {
74+
Spacer()
75+
Text("View all tasks")
76+
.scaledFont(size: 12, weight: .semibold)
77+
.foregroundColor(OmiColors.textSecondary)
78+
Image(systemName: "chevron.right")
79+
.scaledFont(size: 10)
80+
.foregroundColor(OmiColors.textSecondary)
81+
Spacer()
82+
}
7183
}
84+
.buttonStyle(.plain)
85+
.padding(.top, 8)
86+
87+
Spacer(minLength: 0)
7288
}
73-
.buttonStyle(.plain)
74-
.padding(.top, 4)
89+
.frame(maxWidth: .infinity, maxHeight: .infinity)
7590
}
7691
}
7792
.padding(22)

0 commit comments

Comments
 (0)