Skip to content

Commit 806a9fe

Browse files
jasonlongclaude
andcommitted
Stack action toasts as layered cards
Back-to-back done/unsub actions used to stack vertically, hiding the rows behind. Toasts now layer in place with the newest in front; older cards scale down and offset up so a sliver peeks above. Visible stack capped at three. Adds a debug-only Cmd+Shift+T shortcut that pushes a sample toast for verifying the stack effect without dispatching real thread actions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a2a1a51 commit 806a9fe

2 files changed

Lines changed: 56 additions & 3 deletions

File tree

Octodot/App/AppState.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,29 @@ final class AppState {
553553
}
554554
}
555555

556+
#if DEBUG
557+
private static let debugToastSamples: [String] = [
558+
"Marked acme/backend#1042 done",
559+
"Unsubscribed from acme/web#891",
560+
"Marked acme/api-gateway#234 done",
561+
"Unsubscribed from acme/storage#203",
562+
"Marked 4 items done",
563+
"Opened acme/auth#156"
564+
]
565+
private var debugToastCounter = 0
566+
567+
func presentDebugToast() {
568+
let message = Self.debugToastSamples[debugToastCounter % Self.debugToastSamples.count]
569+
debugToastCounter += 1
570+
let toast = ActionToast(message: message)
571+
actionToasts.append(toast)
572+
let toastID = toast.id
573+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
574+
self?.actionToasts.removeAll { $0.id == toastID }
575+
}
576+
}
577+
#endif
578+
556579
private static func toastIdentifier(for notification: GitHubNotification) -> String {
557580
if let reference = notification.displayReferenceNumber {
558581
return "\(notification.repository)\(reference)"

Octodot/Views/PanelContentView.swift

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,15 @@ struct PanelContentView: View {
251251
}
252252

253253
private func handleAppKitKeyEvent(_ event: NSEvent, phase: PanelKeyEventPhase) -> Bool {
254+
#if DEBUG
255+
if phase == .down,
256+
event.modifierFlags.contains(.command),
257+
event.modifierFlags.contains(.shift),
258+
event.charactersIgnoringModifiers?.lowercased() == "t" {
259+
appState.presentDebugToast()
260+
return true
261+
}
262+
#endif
254263
let input = PanelInput.keyInput(for: event)
255264
if phase == .up, suppressedKeyUpInput == input {
256265
suppressedKeyUpInput = nil
@@ -465,17 +474,38 @@ struct PanelContentView: View {
465474
private struct ActionToastStack: View {
466475
let toasts: [AppState.ActionToast]
467476

477+
private static let maxVisible = 3
478+
private static let stackOffset: CGFloat = 7
479+
private static let stackScale: CGFloat = 0.06
480+
481+
private var visibleToasts: [AppState.ActionToast] {
482+
Array(toasts.suffix(Self.maxVisible))
483+
}
484+
468485
var body: some View {
469-
VStack(spacing: 4) {
470-
ForEach(toasts) { toast in
486+
ZStack(alignment: .bottom) {
487+
ForEach(Array(visibleToasts.enumerated()), id: \.element.id) { index, toast in
488+
let depth = visibleToasts.count - 1 - index
471489
ActionToastView(message: toast.message)
490+
.scaleEffect(1 - CGFloat(depth) * Self.stackScale, anchor: .bottom)
491+
.offset(y: -CGFloat(depth) * Self.stackOffset)
492+
.opacity(opacity(forDepth: depth))
493+
.zIndex(Double(index))
472494
.transition(.asymmetric(
473495
insertion: .move(edge: .bottom).combined(with: .opacity),
474496
removal: .opacity
475497
))
476498
}
477499
}
478-
.animation(.spring(response: 0.28, dampingFraction: 0.85), value: toasts)
500+
.animation(.spring(response: 0.32, dampingFraction: 0.85), value: toasts)
501+
}
502+
503+
private func opacity(forDepth depth: Int) -> Double {
504+
switch depth {
505+
case 0: return 1.0
506+
case 1: return 0.85
507+
default: return 0.6
508+
}
479509
}
480510
}
481511

0 commit comments

Comments
 (0)