@@ -107,9 +107,8 @@ struct OverlayView: View {
107107 . font ( . headline)
108108 . lineLimit ( 1 )
109109 Spacer ( minLength: 8 )
110- // The field is multiline (Return inserts a newline), so surface
111- // the submit/cancel keys instead of relying on a bare Return.
112- Text ( " ⌘⏎ save · esc cancel " )
110+ // Enter submits; Shift+Enter inserts a newline; Esc cancels.
111+ Text ( " ⏎ save · ⇧⏎ newline " )
113112 . font ( . caption2)
114113 . foregroundStyle ( . secondary)
115114 . fixedSize ( )
@@ -120,6 +119,13 @@ struct OverlayView: View {
120119 . lineLimit ( 2 ... 5 )
121120 . frame ( width: 260 )
122121 . focused ( $composerFocused)
122+ // Enter submits the note; Shift+Enter falls through to insert a
123+ // newline in the multiline field.
124+ . onKeyPress ( keys: [ . return] ) { key in
125+ if key. modifiers. contains ( . shift) { return . ignored }
126+ addNote ( )
127+ return . handled
128+ }
123129 HStack {
124130 Button ( " Cancel " ) {
125131 comment = " "
@@ -129,33 +135,23 @@ struct OverlayView: View {
129135 // panel is key while typing, so the shortcut reaches this button.
130136 . keyboardShortcut ( . cancelAction)
131137 Spacer ( )
132- Button ( " Add note " ) {
133- // Snapshot the pin anchor BEFORE addNote clears the selection:
134- // element AX top-left minus axOrigin, the same window-local
135- // transform the highlight uses, so the pin lands on the
136- // element's top-left corner.
137- let anchor = session. selected. map {
138- CGPoint ( x: $0. frame. minX - axOrigin. x, y: $0. frame. minY - axOrigin. y)
139- }
140- session. addNote ( comment: comment, anchor: anchor)
141- comment = " "
142- }
143- // Multiline field: Return is a newline, ⌘Return submits.
144- . keyboardShortcut ( . return, modifiers: . command)
145- . disabled ( comment. trimmingCharacters ( in: . whitespacesAndNewlines) . isEmpty)
138+ Button ( " Add note " ) { addNote ( ) }
139+ . buttonStyle ( . borderedProminent)
140+ . disabled ( comment. trimmingCharacters ( in: . whitespacesAndNewlines) . isEmpty)
146141 }
147142 . frame ( width: 260 )
148143 }
149144 . padding ( 12 )
150145 . background ( . regularMaterial, in: RoundedRectangle ( cornerRadius: 10 ) )
151- // Accent caret in the gap, tying the card back to the highlighted
152- // element: it uses the same accent as the highlight stroke, points up
153- // when the card is below (down when flipped above), and slides
154- // horizontally (`caretDX`) to line up with the element's center. Added
155- // before the shadow so the card and caret cast one unified shadow.
146+ // Caret in the gap, tying the card back to the highlighted element. It
147+ // uses the SAME material as the card so it reads as the card's pointer
148+ // (not an accent), points up when the card is below (down when flipped
149+ // above), and slides horizontally (`caretDX`) to line up with the
150+ // element's center. Added before the shadow so card and caret cast one
151+ // unified shadow.
156152 . overlay ( alignment: placement. caretPointsUp ? . top : . bottom) {
157153 ComposerCaret ( pointsUp: placement. caretPointsUp)
158- . fill ( Color . accentColor )
154+ . fill ( . regularMaterial )
159155 . frame ( width: 16 , height: 8 )
160156 . offset ( x: placement. caretDX, y: placement. caretPointsUp ? - 7 : 7 )
161157 }
@@ -187,6 +183,20 @@ struct OverlayView: View {
187183 Task { @MainActor in composerFocused = true }
188184 }
189185
186+ /// Capture the pending note. Snapshots the pin anchor BEFORE `addNote` clears
187+ /// the selection (element AX top-left minus axOrigin — the same window-local
188+ /// transform the highlight uses — so the pin lands on the element's top-left
189+ /// corner), then resets the field. Enter submits; Shift+Enter inserts a
190+ /// newline (handled in the field's `onKeyPress`).
191+ private func addNote( ) {
192+ guard !comment. trimmingCharacters ( in: . whitespacesAndNewlines) . isEmpty else { return }
193+ let anchor = session. selected. map {
194+ CGPoint ( x: $0. frame. minX - axOrigin. x, y: $0. frame. minY - axOrigin. y)
195+ }
196+ session. addNote ( comment: comment, anchor: anchor)
197+ comment = " "
198+ }
199+
190200 /// Estimated card size (260 field + 12 padding each side ≈ 284 wide; a
191201 /// representative height). Only used to clamp the card on-screen and to pick
192202 /// below vs above — the caret attaches to the card's real edge via layout, so
@@ -253,29 +263,35 @@ private struct ToolbarView: View {
253263 let onClose : ( ) -> Void
254264
255265 @Environment ( \. accessibilityReduceMotion) private var reduceMotion
266+ @State private var justCopied = false
256267
257268 private var annotating : Bool { session. mode == . annotating }
258269 private var hasNotes : Bool { !session. pending. isEmpty }
259270
260271 var body : some View {
261272 HStack ( spacing: 2 ) {
262273 PillButton (
263- icon: . pencil,
274+ icon: annotating ? . pencilOff : . pencil,
264275 isActive: annotating,
265276 tooltip: annotating ? " Stop annotating " : " Annotate " ,
266277 action: onToggle
267278 )
268279
269280 if hasNotes {
270- PillButton ( icon: . copy, tooltip: " Copy notes (Markdown) " , action: onCopy)
281+ PillButton (
282+ icon: justCopied ? . check : . copy,
283+ glyphTint: justCopied ? PillStyle . success : nil ,
284+ tooltip: justCopied ? " Copied " : " Copy notes (Markdown) " ,
285+ action: { onCopy ( ) ; flashCopied ( ) }
286+ )
271287 PillButton ( icon: . download, tooltip: " Export to AGENTATION_NOTES.md " , action: onExport)
272288 PillButton ( icon: . trash, isDestructive: true , tooltip: " Clear notes " ) {
273289 session. clear ( )
274290 }
275291 }
276292
277293 divider
278- PillButton ( icon: . close, tooltip: " Close " , action: onClose)
294+ PillButton ( icon: . close, tooltip: " Exit annotate mode " , action: onClose)
279295 }
280296 . padding ( . horizontal, 6 )
281297 . padding ( . vertical, 8 ) // 28pt buttons + 8*2 -> 44pt pill height
@@ -299,6 +315,16 @@ private struct ToolbarView: View {
299315 . animation ( reduceMotion ? nil : . easeOut( duration: 0.15 ) , value: hasNotes)
300316 }
301317
318+ /// Show a green check on the Copy button for a beat as success feedback, then
319+ /// revert to the copy glyph.
320+ private func flashCopied( ) {
321+ justCopied = true
322+ Task { @MainActor in
323+ try ? await Task . sleep ( for: . seconds( 1 ) )
324+ justCopied = false
325+ }
326+ }
327+
302328 /// Compact count bubble that overlaps the pill's top-left corner. A ~18pt
303329 /// accent capsule (grows for multi-digit counts) with white monospaced digits
304330 /// and a thin dark ring for contrast; reuses the accent so it reads as one
@@ -330,13 +356,15 @@ private struct PillButton: View {
330356 let icon : LucideIcon
331357 var isActive : Bool = false
332358 var isDestructive : Bool = false
359+ var glyphTint : Color ? = nil
333360 let tooltip : String
334361 let action : ( ) -> Void
335362
336363 @Environment ( \. accessibilityReduceMotion) private var reduceMotion
337364 @State private var hovering = false
338365
339366 private var glyphColor : Color {
367+ if let glyphTint { return glyphTint }
340368 if isDestructive && hovering { return . white }
341369 if isActive { return . white }
342370 return hovering ? PillStyle . iconHover : PillStyle . iconIdle
@@ -360,7 +388,7 @@ private struct PillButton: View {
360388 . contentShape ( Circle ( ) )
361389 }
362390 . buttonStyle ( PressablePillButtonStyle ( reduceMotion: reduceMotion) )
363- . help ( tooltip)
391+ . pillToolTip ( tooltip)
364392 . accessibilityLabel ( tooltip)
365393 . onHover { value in
366394 guard !reduceMotion else { hovering = value; return }
0 commit comments