@@ -120,10 +120,24 @@ struct ContextDetailsView: View {
120120
121121 @State private var isEditingTitle = false
122122 @State private var draftTitle = " "
123+ // New: clearer editing/hover/focus states for the title
124+ @State private var isHoveringTitle = false
125+ @FocusState private var titleFieldFocused : Bool
123126
124127 // New: Editing state for context items
125128 @State private var editingItemIndex : EditingIndex ? = nil
126129
130+ // Force toolbar relayout when the context or title size category changes
131+ private var principalLayoutID : String {
132+ if isEditingTitle {
133+ // Important: do not depend on draftTitle length while editing to avoid TextField resets
134+ return " \( context. id) -e "
135+ } else {
136+ let bucket = max ( 1 , min ( 8 , context. name. count / 12 ) ) // coarse buckets to reduce churn
137+ return " \( context. id) -v- \( bucket) "
138+ }
139+ }
140+
127141 var body : some View {
128142 ZStack {
129143 VStack ( spacing: 0 ) {
@@ -271,49 +285,108 @@ struct ContextDetailsView: View {
271285 }
272286 }
273287 . toolbar {
274- ToolbarItem ( placement: . navigation) {
288+ // Make the title occupy the principal (expandable) area of the toolbar
289+ ToolbarItem ( placement: . principal) {
275290 if isEditingTitle {
276- TextField ( " Context Name " , text: $draftTitle, onCommit: {
277- isEditingTitle = false
278- let trimmed = draftTitle. trimmingCharacters ( in: . whitespaces)
279- if !trimmed. isEmpty && trimmed != context. name {
280- context. name = trimmed
281- contextManager. saveContexts ( )
291+ HStack ( spacing: 8 ) {
292+ TextField ( " Context Name " , text: $draftTitle, onCommit: { commitTitle ( ) } )
293+ . font ( . system( size: 17 , weight: . bold) )
294+ . textFieldStyle ( PlainTextFieldStyle ( ) )
295+ . focused ( $titleFieldFocused)
296+ . onAppear {
297+ // Focus the field when entering edit mode; draftTitle is set when toggling edit mode
298+ DispatchQueue . main. async { self . titleFieldFocused = true }
299+ }
300+ . onExitCommand { cancelEditTitle ( ) }
301+ . lineLimit ( 1 )
302+ . allowsTightening ( true )
303+ . minimumScaleFactor ( 0.5 )
304+ . fixedSize ( horizontal: false , vertical: true )
305+ . frame ( maxWidth: . infinity, alignment: . leading)
306+ // Quick actions for explicit save/cancel
307+ Button { commitTitle ( ) } label: {
308+ Image ( systemName: " checkmark.circle.fill " ) . foregroundColor ( . accentColor)
282309 }
283- } )
284- . font ( . system( size: 17 , weight: . bold) )
285- . textFieldStyle ( PlainTextFieldStyle ( ) )
286- . frame ( minWidth: 120 , maxWidth: 200 )
287- . onAppear { draftTitle = context. name }
288- . onExitCommand { isEditingTitle = false }
289- } else {
290- Text ( context. name)
291- . font ( . system( size: 17 , weight: . bold) )
292- . onTapGesture {
293- draftTitle = context. name
294- isEditingTitle = true
310+ . buttonStyle ( . plain)
311+ . help ( " Save title " )
312+ Button { cancelEditTitle ( ) } label: {
313+ Image ( systemName: " xmark.circle.fill " ) . foregroundColor ( . secondary)
295314 }
315+ . buttonStyle ( . plain)
316+ . help ( " Cancel editing " )
317+ }
318+ . padding ( . vertical, 4 )
319+ . padding ( . horizontal, 8 )
320+ . background (
321+ RoundedRectangle ( cornerRadius: 8 )
322+ . fill ( Color ( NSColor . textBackgroundColor) )
323+ )
324+ . overlay (
325+ RoundedRectangle ( cornerRadius: 8 )
326+ . stroke ( Color . accentColor. opacity ( 0.6 ) , lineWidth: 1 )
327+ )
328+ // Allow the title editor to expand across available toolbar space
329+ . frame ( minWidth: 140 , maxWidth: . infinity, alignment: . leading)
330+ // Keep stable id logic
331+ . id ( principalLayoutID)
332+ } else {
333+ HStack ( spacing: 6 ) {
334+ Text ( context. name)
335+ . font ( . system( size: 17 , weight: . bold) )
336+ . lineLimit ( 2 )
337+ . truncationMode ( . middle)
338+ . allowsTightening ( true )
339+ . minimumScaleFactor ( 0.5 )
340+ . multilineTextAlignment ( . leading)
341+ . fixedSize ( horizontal: false , vertical: true )
342+ . frame ( maxWidth: . infinity, alignment: . leading)
343+ . contentShape ( Rectangle ( ) )
344+ . onTapGesture {
345+ draftTitle = context. name
346+ isEditingTitle = true
347+ }
348+ // Reserve space for the pencil to avoid layout shifts and only fade it
349+ Image ( systemName: " pencil " )
350+ . font ( . system( size: 13 ) )
351+ . foregroundColor ( . secondary)
352+ . opacity ( isHoveringTitle ? 1 : 0 )
353+ . frame ( width: 16 , height: 16 )
354+ }
355+ . padding ( . vertical, 4 )
356+ . padding ( . horizontal, 8 )
357+ . background (
358+ RoundedRectangle ( cornerRadius: 8 )
359+ . fill ( isHoveringTitle ? Color . accentColor. opacity ( 0.08 ) : Color . clear)
360+ )
361+ . overlay (
362+ RoundedRectangle ( cornerRadius: 8 )
363+ . stroke ( isHoveringTitle ? Color . accentColor. opacity ( 0.25 ) : Color . clear, lineWidth: 1 )
364+ )
365+ // Allow the title to expand across available toolbar space
366+ . frame ( minWidth: 140 , maxWidth: . infinity, alignment: . leading)
367+ . onHover { hovering in
368+ isHoveringTitle = hovering
369+ }
370+ . help ( " Click to rename " )
371+ . id ( principalLayoutID)
296372 }
297373 }
298- ToolbarItemGroup ( placement: . automatic) {
374+ // Ensure action buttons stay on the trailing edge
375+ ToolbarItemGroup ( placement: . primaryAction) {
299376 // Add dropdown menu for adding items (unchanged)
300377 Menu {
301378 Button ( " Application " , action: { showAddAppDialog = true } )
302379 Button ( " Document " , action: { showAddDocumentDialog = true } )
303380 Button ( " Browser Tab " , action: { showAddBrowserTabDialog = true } )
304381 Button ( " Shell Script " , action: { showAddTerminalDialog = true } )
305382 } label: {
306- HStack ( spacing: 6 ) {
307- Image ( systemName: " plus " )
308- Text ( " Add " )
309- }
310- . font ( . system( size: 16 , weight: . medium) )
383+ // HStack(spacing: 6) {
384+ Image ( systemName: " plus " )
385+ // Text("Add")
386+ // }
387+ // .font(.system(size: 16, weight: .medium))
311388 . help ( " Add Item " )
312389 }
313- Rectangle ( )
314- . frame ( width: 1 , height: 24 )
315- . foregroundColor ( Color . gray. opacity ( 0.3 ) )
316- . padding ( . horizontal, 4 )
317390 // Single context button
318391 ContextButton ( context: context, contextManager: contextManager)
319392 }
@@ -343,6 +416,24 @@ struct ContextDetailsView: View {
343416 . listRowInsets ( EdgeInsets ( ) )
344417 . background ( Color . clear)
345418 }
419+
420+ // MARK: - Title helpers
421+ private func commitTitle( ) {
422+ isEditingTitle = false
423+ let trimmed = draftTitle. trimmingCharacters ( in: . whitespacesAndNewlines)
424+ if !trimmed. isEmpty && trimmed != context. name {
425+ context. name = trimmed
426+ contextManager. saveContexts ( )
427+ } else {
428+ // Restore draft to the current name if unchanged
429+ draftTitle = context. name
430+ }
431+ }
432+
433+ private func cancelEditTitle( ) {
434+ draftTitle = context. name
435+ isEditingTitle = false
436+ }
346437}
347438
348439struct ContextButton : View {
0 commit comments