@@ -3,17 +3,22 @@ import SwiftUI
33/// The editor pane. Autosaves the body (debounced ~0.6s, like the web), and a
44/// new note (`note == nil`) is created on first non-empty save then patched —
55/// mirroring the web/iOS create→edit bridge. Pin / archive / trash live in the
6- /// toolbar.
6+ /// toolbar; the title is renamed inline, committing on Enter or focus loss
7+ /// like the web editor's header field.
78struct MacNoteDetail : View {
89 @Environment ( NotesStore . self) private var store
910
1011 let note : Note ?
1112 var onCreated : ( String ) -> Void = { _ in }
1213
1314 @State private var text = " "
15+ @State private var title = " "
16+ @FocusState private var titleFocused : Bool
1417 @State private var createdId : String ?
1518 @State private var saveTask : Task < Void , Never > ?
1619
20+ private let titleCharLimit = 36
21+
1722 /// The live note (existing, or the one we just created), pulled fresh from
1823 /// the store so toolbar state (pin/archive) reflects the latest.
1924 private var current : Note ? {
@@ -23,14 +28,25 @@ struct MacNoteDetail: View {
2328 }
2429
2530 var body : some View {
26- TextEditor ( text: $text)
27- . font ( . body)
28- . textEditorStyle ( . plain)
29- . scrollContentBackground ( . hidden)
30- . padding ( 12 )
31- . navigationTitle ( current? . displayTitle ?? " New Note " )
32- . onAppear { text = note? . body ?? " " }
33- . onChange ( of: text) { _, value in scheduleSave ( value) }
31+ VStack ( alignment: . leading, spacing: 0 ) {
32+ if let n = current {
33+ titleField ( for: n)
34+ }
35+ TextEditor ( text: $text)
36+ . font ( . body)
37+ . textEditorStyle ( . plain)
38+ . scrollContentBackground ( . hidden)
39+ }
40+ . padding ( 12 )
41+ . navigationTitle ( current? . displayTitle ?? " New Note " )
42+ . onAppear {
43+ text = note? . body ?? " "
44+ title = current? . displayTitle ?? " "
45+ }
46+ . onChange ( of: current? . id) { _, _ in
47+ if !titleFocused { title = current? . displayTitle ?? " " }
48+ }
49+ . onChange ( of: text) { _, value in scheduleSave ( value) }
3450 . toolbar {
3551 if let n = current {
3652 ToolbarItemGroup {
@@ -61,6 +77,44 @@ struct MacNoteDetail: View {
6177 }
6278 }
6379
80+ @ViewBuilder
81+ private func titleField( for n: Note ) -> some View {
82+ if n. trashed {
83+ Text ( n. displayTitle)
84+ . font ( . title3. weight ( . semibold) )
85+ . foregroundStyle ( . secondary)
86+ . padding ( . bottom, 8 )
87+ } else {
88+ TextField ( " Title " , text: $title)
89+ . font ( . title3. weight ( . semibold) )
90+ . textFieldStyle ( . plain)
91+ . focused ( $titleFocused)
92+ . onSubmit { titleFocused = false }
93+ . onExitCommand {
94+ title = n. displayTitle
95+ titleFocused = false
96+ }
97+ . onChange ( of: title) { _, value in
98+ if value. count > titleCharLimit {
99+ title = String ( value. prefix ( titleCharLimit) )
100+ }
101+ }
102+ . onChange ( of: titleFocused) { _, focused in
103+ if !focused { commitTitle ( for: n) }
104+ }
105+ . padding ( . bottom, 8 )
106+ }
107+ }
108+
109+ private func commitTitle( for n: Note ) {
110+ let trimmed = title. trimmingCharacters ( in: . whitespaces)
111+ guard !trimmed. isEmpty, trimmed != n. displayTitle else {
112+ title = n. displayTitle
113+ return
114+ }
115+ Task { await store. update ( n. id, patch: [ " title " : trimmed] ) }
116+ }
117+
64118 private func scheduleSave( _ value: String ) {
65119 saveTask? . cancel ( )
66120 saveTask = Task {
0 commit comments