11import SwiftUI
22
33/// Shared provider section header used by the dashboard and its lifted provider-reorder preview.
4- /// The name leads with the optional plan badge beside it; the provider mark sits at the trailing
5- /// edge. Callers supply an optional `warning` — the latest refresh error, rendered as a small amber
4+ /// The provider mark and name lead, followed by the optional plan badge. Dashboard callers supply a
5+ /// screenshot-copy action, revealed at the trailing edge while the header is hovered. Callers can also
6+ /// supply an optional `warning` — the latest refresh error, rendered as a small amber
67/// triangle beside the name whose hover tooltip carries the message (e.g. "Not logged in. Run `codex`
78/// to authenticate."). The
89/// optional `staleness` is the dashboard-only hint that the values shown are an aged snapshot still
@@ -20,28 +21,40 @@ struct ProviderSectionHeader: View {
2021 /// (dashboard only; `nil` in the reorder preview, which never surfaces staleness). Its tooltip carries
2122 /// the precise age.
2223 var staleness : StalenessHint ?
24+ /// Dashboard-only screenshot action. The reorder preview omits it, while Customize uses its own
25+ /// row type and is unaffected by this header.
26+ var onCopyScreenshot : ( ( ) -> Bool ) ?
2327
2428 /// Header type and icon track the density setting like the rows do, so Compact shrinks the
2529 /// whole section anatomy — not just the rows under it.
2630 @AppStorage ( DensitySetting . key) private var density = DensitySetting . regular
2731 /// Party easter egg: pulse the provider mark. Off by default everywhere else.
2832 @Environment ( \. popoverPartyMode) private var partyMode
33+ @State private var isHovered = false
2934
30- init ( provider: Provider , plan: String ? = nil , warning: String ? = nil , refreshing: Bool = false , staleness: StalenessHint ? = nil ) {
35+ init (
36+ provider: Provider ,
37+ plan: String ? = nil ,
38+ warning: String ? = nil ,
39+ refreshing: Bool = false ,
40+ staleness: StalenessHint ? = nil ,
41+ onCopyScreenshot: ( ( ) -> Bool ) ? = nil
42+ ) {
3143 self . provider = provider
3244 self . plan = plan
3345 self . warning = warning
3446 self . refreshing = refreshing
3547 self . staleness = staleness
48+ self . onCopyScreenshot = onCopyScreenshot
3649 }
3750
3851 var body : some View {
3952 HStack ( spacing: 5 ) {
40- // A grip leading the name marks the header line as draggable to reorder providers. The
41- // whole line still carries the gesture; this is only the visual affordance. The trailing
42- // gap keeps it from crowding the provider name beside it.
43- DragHandleGrip ( )
44- . padding ( . trailing , 4 )
53+ // The provider mark replaces the dashboard's visual drag grip. Reordering still belongs
54+ // to the whole header at the caller, so the logo itself stays presentational.
55+ ProviderIcon ( source : provider. icon , inset : 0.04 )
56+ . frame ( width : density . headerIconSize , height : density . headerIconSize )
57+ . partyPulse ( partyMode )
4558 // Baseline-aligned pair: the plan badge (and stale tag) are smaller type and sit on the
4659 // name's text baseline, so the words line up along the bottom rather than floating centered.
4760 HStack ( alignment: . firstTextBaseline, spacing: 5 ) {
@@ -81,18 +94,19 @@ struct ProviderSectionHeader: View {
8194 . accessibilityLabel ( warning)
8295 }
8396 Spacer ( minLength: 8 )
84- // Match the menu-bar strip glyph: a near-zero inset lets the mark fill its box so the
85- // header logo reads at the same scale as the tray, instead of floating small inside the
86- // default list-context padding.
87- ProviderIcon ( source: provider. icon, inset: 0.04 )
88- . frame ( width: density. headerIconSize, height: density. headerIconSize)
89- . partyPulse ( partyMode)
97+ if let onCopyScreenshot {
98+ CopyFeedbackButton (
99+ accessibilityLabel: " Copy \( provider. displayName) Screenshot " ,
100+ isRevealed: isHovered,
101+ action: onCopyScreenshot
102+ )
103+ }
90104 }
91- // Shave the left inset so the leading grip sits a touch closer to the card's edge.
92105 . padding ( . leading, 2 )
93106 . padding ( . trailing, 4 )
94107 . padding ( . vertical, 2 )
95108 . contentShape ( Rectangle ( ) )
109+ . onHover { isHovered = $0 }
96110 }
97111}
98112
@@ -121,28 +135,3 @@ struct ReorderGrip: View {
121135 . contentShape ( Rectangle ( ) )
122136 }
123137}
124-
125- /// The 2×3 dot-grid grip that leads the dashboard provider name. Kept quiet (tertiary) so it reads
126- /// as an affordance hinting the header line can be dragged to reorder providers, not as a control
127- /// competing with the name beside it.
128- struct DragHandleGrip : View {
129- var body : some View {
130- VStack ( spacing: 2 ) {
131- ForEach ( 0 ..< 3 ) { _ in
132- HStack ( spacing: 2 ) {
133- dot
134- dot
135- }
136- }
137- }
138- . frame ( height: 22 )
139- . contentShape ( Rectangle ( ) )
140- . accessibilityHidden ( true )
141- }
142-
143- private var dot : some View {
144- Circle ( )
145- . fill ( . tertiary)
146- . frame ( width: 1.75 , height: 1.75 )
147- }
148- }
0 commit comments