Skip to content

Commit c544e70

Browse files
lividclaude
andcommitted
Enhance QuickPost: auto-expand height, numbered list autocomplete, discard confirmation
- Text area auto-expands from 5 to 12 rows as user types, driven by layout manager content height calculation - Numbered list autocomplete (1. → 2. → 3.) with empty item cancellation - Cancel button shows discard confirmation when post has content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent df27bbf commit c544e70

4 files changed

Lines changed: 69 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
- Xcode is installed under /Applications/Xcode-16.4.0.app/
1+
- When running xcodebuild, prefer using `/Applications/Xcode-16.4.0.app/Contents/Developer` as the developer directory (via `DEVELOPER_DIR` env var or `xcode-select`), falling back to `/Applications/Xcode.app/Contents/Developer` if unavailable.
22
- Must compile for macOS 12

Planet/Labs/Quick Post/QuickPostView.swift

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ struct QuickPostView: View {
1515
_viewModel = StateObject(wrappedValue: QuickPostViewModel.shared)
1616
}
1717

18+
private var textAreaHeight: CGFloat {
19+
let font = NSFont(name: "Menlo", size: 14.0) ?? NSFont.systemFont(ofSize: 14.0)
20+
let lineHeight = ceil(font.ascender - font.descender + font.leading) + 4 // 4 = lineSpacing
21+
let insets: CGFloat = 8 // textContainerInset.height * 2
22+
let padding: CGFloat = 20 // SwiftUI top + bottom padding
23+
let minHeight = lineHeight * 5 + insets + padding
24+
let maxHeight = lineHeight * 12 + insets + padding
25+
let needed = viewModel.textContentHeight + padding
26+
return max(minHeight, min(maxHeight, needed))
27+
}
28+
1829
var body: some View {
1930
VStack(spacing: 0) {
2031
// Upper: Avatar | Text Entry
@@ -41,7 +52,7 @@ struct QuickPostView: View {
4152
.padding(.bottom, 10)
4253
.padding(.leading, 0)
4354
.padding(.trailing, 10)
44-
.frame(height: 160)
55+
.frame(height: textAreaHeight)
4556
.onChange(of: viewModel.content) { newValue in
4657
handleAutocomplete(oldValue: previousContent, newValue: newValue)
4758
previousContent = newValue
@@ -104,8 +115,12 @@ struct QuickPostView: View {
104115
}
105116
Spacer()
106117
Button(role: .cancel) {
107-
viewModel.cleanup()
108-
dismiss()
118+
if viewModel.hasContent {
119+
viewModel.showDiscardAlert = true
120+
} else {
121+
viewModel.cleanup()
122+
dismiss()
123+
}
109124
} label: {
110125
Text("Cancel")
111126
.frame(minWidth: 50)
@@ -133,16 +148,25 @@ struct QuickPostView: View {
133148
}.padding(10)
134149
.background(Color(NSColor.windowBackgroundColor))
135150
}.frame(width: 500, height: sheetHeight())
151+
.alert("Discard Post?", isPresented: $viewModel.showDiscardAlert) {
152+
Button("Discard", role: .destructive) {
153+
viewModel.cleanup()
154+
dismiss()
155+
}
156+
Button("Keep Editing", role: .cancel) {}
157+
} message: {
158+
Text("Your post will be lost if you discard it.")
159+
}
136160
}
137161

138162
private func sheetHeight() -> CGFloat {
139163
if viewModel.fileURLs.count > 0 {
140164
if let _ = viewModel.audioURL {
141-
return 310 + 25
165+
return textAreaHeight + 175
142166
}
143-
return 310
167+
return textAreaHeight + 150
144168
}
145-
return 200
169+
return textAreaHeight + 40
146170
}
147171

148172
private func handleAutocomplete(oldValue: String, newValue: String) {
@@ -161,8 +185,9 @@ struct QuickPostView: View {
161185
let previousLine = lines[lines.count - 2]
162186
let trimmedPrevious = previousLine.trimmingCharacters(in: .whitespaces)
163187

164-
// Check if previous line is an empty list item (including todo lists)
165-
if trimmedPrevious == "*" || trimmedPrevious == "-" || trimmedPrevious == "- [ ]" || trimmedPrevious == "- [x]" {
188+
// Check if previous line is an empty list item (including todo lists and numbered lists)
189+
let isEmptyNumberedItem = trimmedPrevious.range(of: #"^\d+\.$"#, options: .regularExpression) != nil
190+
if trimmedPrevious == "*" || trimmedPrevious == "-" || trimmedPrevious == "- [ ]" || trimmedPrevious == "- [x]" || isEmptyNumberedItem {
166191
// Remove the empty list marker from previous line
167192
var updatedLines = lines
168193
updatedLines[lines.count - 2] = ""
@@ -189,6 +214,15 @@ struct QuickPostView: View {
189214
viewModel.content = newValue + "- "
190215
}
191216
}
217+
// Check if previous line starts with a numbered list (e.g., "1. ")
218+
else if let match = trimmedPrevious.range(of: #"^(\d+)\. "#, options: .regularExpression) {
219+
if newValue.hasSuffix("\n") {
220+
let numberStr = trimmedPrevious[match].dropLast(2) // drop ". "
221+
if let number = Int(numberStr) {
222+
viewModel.content = newValue + "\(number + 1). "
223+
}
224+
}
225+
}
192226
}
193227

194228
@ViewBuilder
@@ -432,6 +466,15 @@ struct QuickPostTextView: NSViewRepresentable {
432466
return
433467
}
434468
parent.text = textView.string
469+
updateContentHeight(textView)
470+
}
471+
472+
private func updateContentHeight(_ textView: NSTextView) {
473+
guard let layoutManager = textView.layoutManager,
474+
let textContainer = textView.textContainer else { return }
475+
layoutManager.ensureLayout(for: textContainer)
476+
let usedRect = layoutManager.usedRect(for: textContainer)
477+
parent.viewModel.textContentHeight = usedRect.height + textView.textContainerInset.height * 2
435478
}
436479

437480
func textDidEndEditing(_ notification: Notification) {
@@ -542,6 +585,15 @@ final class QuickPostTextEditorContainer: NSView {
542585
textView.string = text
543586
let end = (text as NSString).length
544587
textView.setSelectedRange(NSRange(location: end, length: 0))
588+
updateContentHeight()
589+
}
590+
591+
private func updateContentHeight() {
592+
guard let layoutManager = textView.layoutManager,
593+
let textContainer = textView.textContainer else { return }
594+
layoutManager.ensureLayout(for: textContainer)
595+
let usedRect = layoutManager.usedRect(for: textContainer)
596+
viewModel.textContentHeight = usedRect.height + textView.textContainerInset.height * 2
545597
}
546598
}
547599

Planet/Labs/Quick Post/QuickPostViewModel.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ class QuickPostViewModel: ObservableObject {
5454
@Published var allowMultipleSelection = false
5555

5656
@Published var content: String = ""
57+
@Published var textContentHeight: CGFloat = 0
58+
@Published var showDiscardAlert: Bool = false
59+
60+
var hasContent: Bool {
61+
!content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !fileURLs.isEmpty
62+
}
5763

5864
@Published var heroImage: String? = nil
5965
@Published var fileURLs: [URL] = []
@@ -140,6 +146,7 @@ class QuickPostViewModel: ObservableObject {
140146
}
141147
temporaryFileURLs.removeAll()
142148
content = ""
149+
textContentHeight = 0
143150
heroImage = nil
144151
fileURLs = []
145152
audioURL = nil

Planet/versioning.xcconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
CURRENT_PROJECT_VERSION = 2635
1+
CURRENT_PROJECT_VERSION = 2636

0 commit comments

Comments
 (0)