Files
failwell/macos/Sources/FailWell/Views/TaskDetailView.swift
Julien Calixte 29a71a73b2 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.
2026-05-15 11:27:51 +02:00

133 lines
4.7 KiB
Swift

import SwiftUI
struct TaskDetailView: View {
@Environment(AppStore.self) private var store
@Environment(Router.self) private var router
let taskId: String
@FocusState private var focused: Bool
@State private var showDeleteConfirm = false
private var task: TaskPlan? { store.task(id: taskId) }
var body: some View {
if let task {
VStack(spacing: 0) {
header(task)
Divider()
stepList(task)
Divider()
footer
}
.focusable()
.focused($focused)
.focusEffectDisabled()
.onAppear { focused = true }
.onKeyPress(.init("s")) { startSession(); return .handled }
.onKeyPress(.init("e"), phases: .down) { event in
if event.modifiers.contains(.command) {
router.push(.editor(taskId: task.id))
return .handled
}
return .ignored
}
.onKeyPress(.init("h"), phases: .down) { event in
if event.modifiers.contains(.command) {
router.push(.history(taskId: task.id))
return .handled
}
return .ignored
}
.onKeyPress(.init("d"), phases: .down) { event in
if event.modifiers.contains(.command) {
if let copy = store.duplicateTask(id: task.id) {
router.replace(with: .detail(taskId: copy.id))
}
return .handled
}
return .ignored
}
.onKeyPress(.delete) {
showDeleteConfirm = true
return .handled
}
.confirmationDialog("Delete \(task.title)?", isPresented: $showDeleteConfirm) {
Button("Delete", role: .destructive) {
store.deleteTask(id: task.id)
router.popToRoot()
}
Button("Cancel", role: .cancel) {}
}
} else {
ContentUnavailableView("Task not found", systemImage: "questionmark.folder")
.onAppear { router.popToRoot() }
}
}
private func header(_ task: TaskPlan) -> some View {
HStack(spacing: 8) {
Button { router.pop() } label: {
Image(systemName: "chevron.left").foregroundStyle(Theme.dim)
}
.buttonStyle(.plain)
.keyboardShortcut(.leftArrow, modifiers: [])
Text(task.title).monoTitle().lineLimit(1)
Spacer()
Text("\(task.totalEstimation)m").monoSmall().foregroundStyle(Theme.dim)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
@ViewBuilder
private func stepList(_ task: TaskPlan) -> some View {
if task.steps.isEmpty {
VStack(spacing: 8) {
Text("no steps").monoBody().foregroundStyle(Theme.dim)
Text("⌘E add steps").monoSmall().foregroundStyle(Theme.dim)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(task.steps) { step in
HStack(spacing: 8) {
Text(Theme.Glyph.pending).foregroundStyle(Theme.dim)
Text(step.title).monoBody().lineLimit(1)
Spacer()
Text("\(step.estimation)m").monoSmall().foregroundStyle(Theme.dim)
}
.padding(.horizontal, 12)
.frame(height: Theme.rowHeight)
}
}
}
}
}
private var footer: some View {
HStack(spacing: 12) {
Text("s start").monoSmall().foregroundStyle(Theme.dim)
Text("⌘E edit").monoSmall().foregroundStyle(Theme.dim)
Text("⌘H history").monoSmall().foregroundStyle(Theme.dim)
Text("⌘D duplicate").monoSmall().foregroundStyle(Theme.dim)
Text("⌫ delete").monoSmall().foregroundStyle(Theme.dim)
Spacer()
Text("esc back").monoSmall().foregroundStyle(Theme.dim)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
}
private func startSession() {
guard let task, !task.steps.isEmpty else { return }
store.initRecord(taskId: task.id)
// Reset any prior end and start fresh.
store.reset(taskId: task.id)
store.startStepRecord(taskId: task.id, stepId: task.steps[0].id)
router.push(.record(taskId: task.id))
}
}