Skip to content

Commit 43ea2e3

Browse files
fjbarrettclaude
andauthored
feat: rename a note's title inline in the Mac editor (#321)
The web editor renames titles from its header field; the Mac detail pane only displayed the inferred title. An editable title now sits above the body — commit on Enter or focus loss, Escape reverts, 36-char cap to match the web's TITLE_CHAR_LIMIT, read-only in Trash. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 71466a1 commit 43ea2e3

2 files changed

Lines changed: 63 additions & 9 deletions

File tree

ios/KeepMac/MacNoteDetail.swift

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
78
struct 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 {
185 KB
Loading

0 commit comments

Comments
 (0)