feat(macos): add native menu bar app with notch overlay

Introduces a SwiftUI companion app for the Fail Well PWA: menu bar
status item, popover with task list / record / history views, command
palette, and a Dynamic Island-style notch overlay that shows the active
step and timer beside the hardware notch and expands on hover to reveal
pause/next controls.
This commit is contained in:
Julien Calixte
2026-05-15 11:27:51 +02:00
parent 0c0d555ac1
commit 29a71a73b2
33 changed files with 2602 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import Foundation
struct Step: Codable, Identifiable, Hashable, Sendable {
let id: String
var title: String
/// estimation in minutes
var estimation: Int
}

View File

@@ -0,0 +1,54 @@
import Foundation
/// A planned task with a title, optional link, and an append-only history of step lists.
/// Named `TaskPlan` to avoid collision with `_Concurrency.Task`.
struct TaskPlan: Codable, Identifiable, Hashable, Sendable {
let id: String
var title: String
var date: Date
var link: String?
var stepHistory: [[Step]]
init(id: String, title: String, date: Date = Date(), link: String? = nil, stepHistory: [[Step]] = []) {
self.id = id
self.title = title
self.date = date
self.link = link
self.stepHistory = stepHistory
}
var steps: [Step] {
stepHistory.last ?? []
}
var initialPlan: [Step] {
stepHistory.first ?? []
}
var wasUpdated: Bool {
stepHistory.count > 1
}
var totalEstimation: Int {
steps.reduce(0) { $0 + $1.estimation }
}
mutating func newSteps(_ steps: [Step]) {
stepHistory.append(steps)
}
mutating func editSteps(_ steps: [Step]) {
stepHistory.append(self.steps + steps)
}
mutating func updateSteps(_ steps: [Step]) {
stepHistory.append(steps)
}
mutating func removeStep(at index: Int) {
guard index >= 0, index < steps.count else { return }
stepHistory.append(steps.enumerated().compactMap { i, step in
i == index ? nil : step
})
}
}

View File

@@ -0,0 +1,41 @@
import Foundation
struct TaskRecord: Codable, Hashable, Sendable {
let taskId: String
var start: Date
var end: Date?
var stepRecords: [String: TimeRange]
var notes: String
var breakTime: TimeRange?
var currentStepId: String?
init(
taskId: String,
start: Date = Date(),
end: Date? = nil,
stepRecords: [String: TimeRange] = [:],
notes: String = "",
breakTime: TimeRange? = nil,
currentStepId: String? = nil
) {
self.taskId = taskId
self.start = start
self.end = end
self.stepRecords = stepRecords
self.notes = notes
self.breakTime = breakTime
self.currentStepId = currentStepId
}
var isActive: Bool {
end == nil && currentStepId != nil
}
var isPaused: Bool {
breakTime != nil && breakTime?.end == nil
}
var hasStarted: Bool {
!stepRecords.isEmpty
}
}

View File

@@ -0,0 +1,11 @@
import Foundation
struct TimeRange: Codable, Hashable, Sendable {
var start: Date
var end: Date?
var duration: TimeInterval? {
guard let end else { return nil }
return end.timeIntervalSince(start)
}
}