Browse Source

Switch text input counters to word limits and move them outside the field.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 tháng trước cách đây
mục cha
commit
0c3c942875

+ 38 - 0
gramora/Utilities/String+WordCount.swift

@@ -0,0 +1,38 @@
+import Foundation
+
+extension String {
+    var wordCount: Int {
+        let trimmed = trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !trimmed.isEmpty else { return 0 }
+        return trimmed
+            .components(separatedBy: .whitespacesAndNewlines)
+            .filter { !$0.isEmpty }
+            .count
+    }
+
+    func truncated(toMaxWords maxWords: Int) -> String {
+        guard maxWords > 0 else { return "" }
+        guard wordCount > maxWords else { return self }
+
+        var count = 0
+        var index = startIndex
+
+        while index < endIndex {
+            while index < endIndex, self[index].isWhitespace {
+                index = self.index(after: index)
+            }
+            guard index < endIndex else { break }
+
+            count += 1
+            while index < endIndex, !self[index].isWhitespace {
+                index = self.index(after: index)
+            }
+
+            if count >= maxWords {
+                return String(self[..<index])
+            }
+        }
+
+        return self
+    }
+}

+ 4 - 6
gramora/ViewModels/EmailWriterViewModel.swift

@@ -4,7 +4,7 @@ import Foundation
 
 @MainActor
 final class EmailWriterViewModel: ObservableObject {
-    static let maxCharacters = 5_000
+    static let maxWords = 5_000
 
     @Published var subject: String = ""
     @Published var prompt: String = ""
@@ -25,10 +25,8 @@ final class EmailWriterViewModel: ObservableObject {
         self.historyStorage = historyStorage
     }
 
-    var characterCount: Int { prompt.count }
-
-    var characterCountLabel: String {
-        "\(characterCount)/\(Self.maxCharacters)"
+    var wordCountLimitLabel: String {
+        "\(prompt.wordCount)/\(Self.maxWords)"
     }
 
     var canGenerate: Bool {
@@ -41,7 +39,7 @@ final class EmailWriterViewModel: ObservableObject {
 
     func pastePrompt() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
-        prompt = String(clipboard.prefix(Self.maxCharacters))
+        prompt = clipboard.truncated(toMaxWords: Self.maxWords)
         generatedEmail = ""
         errorMessage = nil
     }

+ 4 - 6
gramora/ViewModels/GrammarCheckerViewModel.swift

@@ -4,7 +4,7 @@ import Foundation
 
 @MainActor
 final class GrammarCheckerViewModel: ObservableObject {
-    static let maxCharacters = 10_000
+    static let maxWords = 10_000
 
     @Published var text: String = ""
     @Published private(set) var issues: [GrammarIssue] = []
@@ -24,10 +24,8 @@ final class GrammarCheckerViewModel: ObservableObject {
         self.historyStorage = historyStorage
     }
 
-    var characterCount: Int { text.count }
-
-    var characterCountLabel: String {
-        "\(characterCount)/\(Self.maxCharacters)"
+    var wordCountLimitLabel: String {
+        "\(text.wordCount)/\(Self.maxWords)"
     }
 
     var issueCountLabel: String {
@@ -41,7 +39,7 @@ final class GrammarCheckerViewModel: ObservableObject {
 
     func pasteText() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
-        text = String(clipboard.prefix(Self.maxCharacters))
+        text = clipboard.truncated(toMaxWords: Self.maxWords)
         resetResults()
     }
 

+ 4 - 6
gramora/ViewModels/JournalFinderViewModel.swift

@@ -5,7 +5,7 @@ import Foundation
 @MainActor
 final class JournalFinderViewModel: ObservableObject {
     static let maxTitleLength = 300
-    static let maxAbstractLength = 10_000
+    static let maxAbstractWords = 10_000
     static let maxKeywordsLength = 500
 
     @Published var title: String = ""
@@ -31,10 +31,8 @@ final class JournalFinderViewModel: ObservableObject {
         self.historyStorage = historyStorage
     }
 
-    var abstractCharacterCount: Int { abstractText.count }
-
-    var abstractCharacterCountLabel: String {
-        "\(abstractCharacterCount)/\(Self.maxAbstractLength)"
+    var abstractWordCountLimitLabel: String {
+        "\(abstractText.wordCount)/\(Self.maxAbstractWords)"
     }
 
     var canSearch: Bool {
@@ -56,7 +54,7 @@ final class JournalFinderViewModel: ObservableObject {
 
     func pasteAbstract() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
-        abstractText = String(clipboard.prefix(Self.maxAbstractLength))
+        abstractText = clipboard.truncated(toMaxWords: Self.maxAbstractWords)
         resetResults()
     }
 

+ 5 - 7
gramora/ViewModels/LanguageTranslatorViewModel.swift

@@ -5,7 +5,7 @@ import UniformTypeIdentifiers
 
 @MainActor
 final class LanguageTranslatorViewModel: ObservableObject {
-    static let maxCharacters = 10_000
+    static let maxWords = 10_000
 
     @Published var sourceText: String = ""
     @Published var translatedText: String = ""
@@ -25,10 +25,8 @@ final class LanguageTranslatorViewModel: ObservableObject {
         self.historyStorage = historyStorage
     }
 
-    var characterCount: Int { sourceText.count }
-
-    var characterCountLabel: String {
-        "\(characterCount)/\(Self.maxCharacters)"
+    var wordCountLimitLabel: String {
+        "\(sourceText.wordCount)/\(Self.maxWords)"
     }
 
     var canTranslate: Bool {
@@ -45,7 +43,7 @@ final class LanguageTranslatorViewModel: ObservableObject {
 
     func pasteText() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
-        sourceText = String(clipboard.prefix(Self.maxCharacters))
+        sourceText = clipboard.truncated(toMaxWords: Self.maxWords)
         translatedText = ""
         errorMessage = nil
     }
@@ -61,7 +59,7 @@ final class LanguageTranslatorViewModel: ObservableObject {
 
         do {
             let contents = try String(contentsOf: url, encoding: .utf8)
-            sourceText = String(contents.prefix(Self.maxCharacters))
+            sourceText = contents.truncated(toMaxWords: Self.maxWords)
             translatedText = ""
             errorMessage = nil
         } catch {

+ 4 - 6
gramora/ViewModels/ParaphrasingViewModel.swift

@@ -4,7 +4,7 @@ import Foundation
 
 @MainActor
 final class ParaphrasingViewModel: ObservableObject {
-    static let maxCharacters = 10_000
+    static let maxWords = 10_000
 
     @Published var text: String = ""
     @Published private(set) var paraphrasedText: String = ""
@@ -17,10 +17,8 @@ final class ParaphrasingViewModel: ObservableObject {
         self.paraphrasingService = paraphrasingService
     }
 
-    var characterCount: Int { text.count }
-
-    var characterCountLabel: String {
-        "\(characterCount)/\(Self.maxCharacters)"
+    var wordCountLimitLabel: String {
+        "\(text.wordCount)/\(Self.maxWords)"
     }
 
     var canParaphrase: Bool {
@@ -29,7 +27,7 @@ final class ParaphrasingViewModel: ObservableObject {
 
     func pasteText() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
-        text = String(clipboard.prefix(Self.maxCharacters))
+        text = clipboard.truncated(toMaxWords: Self.maxWords)
         paraphrasedText = ""
         errorMessage = nil
     }

+ 4 - 6
gramora/ViewModels/PunctuationCheckerViewModel.swift

@@ -4,7 +4,7 @@ import Foundation
 
 @MainActor
 final class PunctuationCheckerViewModel: ObservableObject {
-    static let maxCharacters = 10_000
+    static let maxWords = 10_000
 
     @Published var text: String = ""
     @Published private(set) var correctedText: String = ""
@@ -19,10 +19,8 @@ final class PunctuationCheckerViewModel: ObservableObject {
         self.punctuationCheckService = punctuationCheckService
     }
 
-    var characterCount: Int { text.count }
-
-    var characterCountLabel: String {
-        "\(characterCount)/\(Self.maxCharacters)"
+    var wordCountLimitLabel: String {
+        "\(text.wordCount)/\(Self.maxWords)"
     }
 
     var canCheck: Bool {
@@ -31,7 +29,7 @@ final class PunctuationCheckerViewModel: ObservableObject {
 
     func pasteText() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
-        text = String(clipboard.prefix(Self.maxCharacters))
+        text = clipboard.truncated(toMaxWords: Self.maxWords)
         resetResults()
     }
 

+ 4 - 6
gramora/ViewModels/SpellCheckerViewModel.swift

@@ -4,7 +4,7 @@ import Foundation
 
 @MainActor
 final class SpellCheckerViewModel: ObservableObject {
-    static let maxCharacters = 10_000
+    static let maxWords = 10_000
 
     @Published var text: String = ""
     @Published private(set) var mistakes: [SpellingMistake] = []
@@ -24,10 +24,8 @@ final class SpellCheckerViewModel: ObservableObject {
         self.historyStorage = historyStorage
     }
 
-    var characterCount: Int { text.count }
-
-    var characterCountLabel: String {
-        "\(characterCount)/\(Self.maxCharacters)"
+    var wordCountLimitLabel: String {
+        "\(text.wordCount)/\(Self.maxWords)"
     }
 
     var mistakeCountLabel: String {
@@ -41,7 +39,7 @@ final class SpellCheckerViewModel: ObservableObject {
 
     func pasteText() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
-        text = String(clipboard.prefix(Self.maxCharacters))
+        text = clipboard.truncated(toMaxWords: Self.maxWords)
         resetResults()
     }
 

+ 4 - 6
gramora/ViewModels/SummarizerViewModel.swift

@@ -4,7 +4,7 @@ import Foundation
 
 @MainActor
 final class SummarizerViewModel: ObservableObject {
-    static let maxCharacters = 10_000
+    static let maxWords = 10_000
 
     @Published var text: String = ""
     @Published private(set) var summary: String = ""
@@ -17,10 +17,8 @@ final class SummarizerViewModel: ObservableObject {
         self.summarizerService = summarizerService
     }
 
-    var characterCount: Int { text.count }
-
-    var characterCountLabel: String {
-        "\(characterCount)/\(Self.maxCharacters)"
+    var wordCountLimitLabel: String {
+        "\(text.wordCount)/\(Self.maxWords)"
     }
 
     var canSummarize: Bool {
@@ -29,7 +27,7 @@ final class SummarizerViewModel: ObservableObject {
 
     func pasteText() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
-        text = String(clipboard.prefix(Self.maxCharacters))
+        text = clipboard.truncated(toMaxWords: Self.maxWords)
         summary = ""
         errorMessage = nil
     }

+ 5 - 10
gramora/ViewModels/WordsCountViewModel.swift

@@ -4,23 +4,18 @@ import Foundation
 
 @MainActor
 final class WordsCountViewModel: ObservableObject {
-    static let maxCharacters = 50_000
+    static let maxWords = 50_000
 
     @Published var text: String = ""
 
     var characterCount: Int { text.count }
 
-    var characterCountLabel: String {
-        "\(characterCount)/\(Self.maxCharacters)"
+    var wordCountLimitLabel: String {
+        "\(wordCount)/\(Self.maxWords)"
     }
 
     var wordCount: Int {
-        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
-        guard !trimmed.isEmpty else { return 0 }
-        return trimmed
-            .components(separatedBy: .whitespacesAndNewlines)
-            .filter { !$0.isEmpty }
-            .count
+        text.wordCount
     }
 
     var wordCountLabel: String {
@@ -46,7 +41,7 @@ final class WordsCountViewModel: ObservableObject {
 
     func pasteText() {
         guard let clipboard = NSPasteboard.general.string(forType: .string) else { return }
-        text = String(clipboard.prefix(Self.maxCharacters))
+        text = clipboard.truncated(toMaxWords: Self.maxWords)
     }
 
     func clearText() {

+ 5 - 6
gramora/Views/EmailWriterView.swift

@@ -45,8 +45,8 @@ struct EmailWriterView: View {
             .padding(.bottom, dynamicBottomPadding)
         }
         .onChange(of: viewModel.prompt) { newValue in
-            if newValue.count > EmailWriterViewModel.maxCharacters {
-                viewModel.prompt = String(newValue.prefix(EmailWriterViewModel.maxCharacters))
+            if newValue.wordCount > EmailWriterViewModel.maxWords {
+                viewModel.prompt = newValue.truncated(toMaxWords: EmailWriterViewModel.maxWords)
             }
         }
         .onAppear(perform: restoreIfNeeded)
@@ -173,7 +173,7 @@ struct EmailWriterView: View {
                 }
             }
 
-            ZStack(alignment: .bottomTrailing) {
+            VStack(alignment: .leading, spacing: 6) {
                 ThinCaretTextEditor(
                     text: $viewModel.prompt,
                     placeholder: "Describe the email you need — key points, recipient context, or a rough draft...",
@@ -190,11 +190,10 @@ struct EmailWriterView: View {
                             .stroke(AppTheme.inputBorder, lineWidth: 1)
                     )
 
-                Text(viewModel.characterCountLabel)
+                Text(viewModel.wordCountLimitLabel)
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
-                    .padding(.trailing, 16)
-                    .padding(.bottom, 12)
+                    .frame(maxWidth: .infinity, alignment: .trailing)
             }
             .frame(maxHeight: .infinity)
         }

+ 5 - 6
gramora/Views/GrammarCheckerView.swift

@@ -65,8 +65,8 @@ struct GrammarCheckerView: View {
             }
         }
         .onChange(of: viewModel.text) { newValue in
-            if newValue.count > GrammarCheckerViewModel.maxCharacters {
-                viewModel.text = String(newValue.prefix(GrammarCheckerViewModel.maxCharacters))
+            if newValue.wordCount > GrammarCheckerViewModel.maxWords {
+                viewModel.text = newValue.truncated(toMaxWords: GrammarCheckerViewModel.maxWords)
             }
             if viewModel.hasResults {
                 viewModel.resetResults()
@@ -133,7 +133,7 @@ struct GrammarCheckerView: View {
                 .clipped()
                 .accessibilityLabel("Your Content")
 
-            ZStack(alignment: .bottomTrailing) {
+            VStack(alignment: .leading, spacing: 6) {
                 ThinCaretTextEditor(
                     text: $viewModel.text,
                     placeholder: "Type or paste your text here and press the check button..."
@@ -148,11 +148,10 @@ struct GrammarCheckerView: View {
                             .stroke(AppTheme.inputBorder, lineWidth: 1)
                     )
 
-                Text(viewModel.characterCountLabel)
+                Text(viewModel.wordCountLimitLabel)
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
-                    .padding(.trailing, 16)
-                    .padding(.bottom, 12)
+                    .frame(maxWidth: .infinity, alignment: .trailing)
             }
 
             HStack(spacing: 8) {

+ 3 - 3
gramora/Views/JournalFinderView.swift

@@ -45,8 +45,8 @@ struct JournalFinderView: View {
             if viewModel.hasResults { viewModel.resetResults() }
         }
         .onChange(of: viewModel.abstractText) { newValue in
-            if newValue.count > JournalFinderViewModel.maxAbstractLength {
-                viewModel.abstractText = String(newValue.prefix(JournalFinderViewModel.maxAbstractLength))
+            if newValue.wordCount > JournalFinderViewModel.maxAbstractWords {
+                viewModel.abstractText = newValue.truncated(toMaxWords: JournalFinderViewModel.maxAbstractWords)
             }
             if viewModel.hasResults { viewModel.resetResults() }
         }
@@ -268,7 +268,7 @@ struct JournalFinderView: View {
                     .stroke(AppTheme.inputBorder, lineWidth: 1)
             )
 
-            Text(viewModel.abstractCharacterCountLabel)
+            Text(viewModel.abstractWordCountLimitLabel)
                 .font(.system(size: 12))
                 .foregroundStyle(AppTheme.textMuted)
                 .frame(maxWidth: .infinity, alignment: .trailing)

+ 5 - 6
gramora/Views/LanguageTranslatorView.swift

@@ -31,8 +31,8 @@ struct LanguageTranslatorView: View {
             .padding(.bottom, dynamicBottomPadding)
         }
         .onChange(of: viewModel.sourceText) { newValue in
-            if newValue.count > LanguageTranslatorViewModel.maxCharacters {
-                viewModel.sourceText = String(newValue.prefix(LanguageTranslatorViewModel.maxCharacters))
+            if newValue.wordCount > LanguageTranslatorViewModel.maxWords {
+                viewModel.sourceText = newValue.truncated(toMaxWords: LanguageTranslatorViewModel.maxWords)
             }
         }
         .onChange(of: viewModel.sourceLanguage) { _ in
@@ -149,7 +149,7 @@ struct LanguageTranslatorView: View {
                 }
             }
 
-            ZStack(alignment: .bottomTrailing) {
+            VStack(alignment: .leading, spacing: 6) {
                 ThinCaretTextEditor(
                     text: $viewModel.sourceText,
                     placeholder: "Start writing...",
@@ -166,11 +166,10 @@ struct LanguageTranslatorView: View {
                             .stroke(AppTheme.inputBorder, lineWidth: 1)
                     )
 
-                Text(viewModel.characterCountLabel)
+                Text(viewModel.wordCountLimitLabel)
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
-                    .padding(.trailing, 16)
-                    .padding(.bottom, 12)
+                    .frame(maxWidth: .infinity, alignment: .trailing)
             }
             .frame(maxHeight: .infinity)
         }

+ 5 - 6
gramora/Views/ParaphrasingView.swift

@@ -43,8 +43,8 @@ struct ParaphrasingView: View {
             }
         }
         .onChange(of: viewModel.text) { newValue in
-            if newValue.count > ParaphrasingViewModel.maxCharacters {
-                viewModel.text = String(newValue.prefix(ParaphrasingViewModel.maxCharacters))
+            if newValue.wordCount > ParaphrasingViewModel.maxWords {
+                viewModel.text = newValue.truncated(toMaxWords: ParaphrasingViewModel.maxWords)
             }
         }
     }
@@ -92,7 +92,7 @@ struct ParaphrasingView: View {
                 .clipped()
                 .accessibilityLabel("Your Content")
 
-            ZStack(alignment: .bottomTrailing) {
+            VStack(alignment: .leading, spacing: 6) {
                 ThinCaretTextEditor(
                     text: $viewModel.text,
                     placeholder: "Type or paste your text here and press the paraphrase button..."
@@ -107,11 +107,10 @@ struct ParaphrasingView: View {
                             .stroke(AppTheme.inputBorder, lineWidth: 1)
                     )
 
-                Text(viewModel.characterCountLabel)
+                Text(viewModel.wordCountLimitLabel)
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
-                    .padding(.trailing, 16)
-                    .padding(.bottom, 12)
+                    .frame(maxWidth: .infinity, alignment: .trailing)
             }
 
             HStack(spacing: 8) {

+ 5 - 6
gramora/Views/PunctuationCheckerView.swift

@@ -43,8 +43,8 @@ struct PunctuationCheckerView: View {
             }
         }
         .onChange(of: viewModel.text) { newValue in
-            if newValue.count > PunctuationCheckerViewModel.maxCharacters {
-                viewModel.text = String(newValue.prefix(PunctuationCheckerViewModel.maxCharacters))
+            if newValue.wordCount > PunctuationCheckerViewModel.maxWords {
+                viewModel.text = newValue.truncated(toMaxWords: PunctuationCheckerViewModel.maxWords)
             }
         }
     }
@@ -92,7 +92,7 @@ struct PunctuationCheckerView: View {
                 .clipped()
                 .accessibilityLabel("Your Content")
 
-            ZStack(alignment: .bottomTrailing) {
+            VStack(alignment: .leading, spacing: 6) {
                 ThinCaretTextEditor(
                     text: $viewModel.text,
                     placeholder: "Type or paste your text here and press the check button..."
@@ -107,11 +107,10 @@ struct PunctuationCheckerView: View {
                             .stroke(AppTheme.inputBorder, lineWidth: 1)
                     )
 
-                Text(viewModel.characterCountLabel)
+                Text(viewModel.wordCountLimitLabel)
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
-                    .padding(.trailing, 16)
-                    .padding(.bottom, 12)
+                    .frame(maxWidth: .infinity, alignment: .trailing)
             }
 
             HStack(spacing: 8) {

+ 5 - 6
gramora/Views/SpellCheckerView.swift

@@ -65,8 +65,8 @@ struct SpellCheckerView: View {
             }
         }
         .onChange(of: viewModel.text) { newValue in
-            if newValue.count > SpellCheckerViewModel.maxCharacters {
-                viewModel.text = String(newValue.prefix(SpellCheckerViewModel.maxCharacters))
+            if newValue.wordCount > SpellCheckerViewModel.maxWords {
+                viewModel.text = newValue.truncated(toMaxWords: SpellCheckerViewModel.maxWords)
             }
             if viewModel.hasResults {
                 viewModel.resetResults()
@@ -133,7 +133,7 @@ struct SpellCheckerView: View {
                 .clipped()
                 .accessibilityLabel("Your Content")
 
-            ZStack(alignment: .bottomTrailing) {
+            VStack(alignment: .leading, spacing: 6) {
                 ThinCaretTextEditor(
                     text: $viewModel.text,
                     placeholder: "Type or paste your text here and press the check button..."
@@ -148,11 +148,10 @@ struct SpellCheckerView: View {
                             .stroke(AppTheme.inputBorder, lineWidth: 1)
                     )
 
-                Text(viewModel.characterCountLabel)
+                Text(viewModel.wordCountLimitLabel)
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
-                    .padding(.trailing, 16)
-                    .padding(.bottom, 12)
+                    .frame(maxWidth: .infinity, alignment: .trailing)
             }
 
             HStack(spacing: 8) {

+ 5 - 6
gramora/Views/SummarizerView.swift

@@ -43,8 +43,8 @@ struct SummarizerView: View {
             }
         }
         .onChange(of: viewModel.text) { newValue in
-            if newValue.count > SummarizerViewModel.maxCharacters {
-                viewModel.text = String(newValue.prefix(SummarizerViewModel.maxCharacters))
+            if newValue.wordCount > SummarizerViewModel.maxWords {
+                viewModel.text = newValue.truncated(toMaxWords: SummarizerViewModel.maxWords)
             }
         }
     }
@@ -92,7 +92,7 @@ struct SummarizerView: View {
                 .clipped()
                 .accessibilityLabel("Your Content")
 
-            ZStack(alignment: .bottomTrailing) {
+            VStack(alignment: .leading, spacing: 6) {
                 ThinCaretTextEditor(
                     text: $viewModel.text,
                     placeholder: "Type or paste your text here and press the summarize button..."
@@ -107,11 +107,10 @@ struct SummarizerView: View {
                             .stroke(AppTheme.inputBorder, lineWidth: 1)
                     )
 
-                Text(viewModel.characterCountLabel)
+                Text(viewModel.wordCountLimitLabel)
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
-                    .padding(.trailing, 16)
-                    .padding(.bottom, 12)
+                    .frame(maxWidth: .infinity, alignment: .trailing)
             }
 
             HStack(spacing: 8) {

+ 5 - 6
gramora/Views/WordsCountView.swift

@@ -23,8 +23,8 @@ struct WordsCountView: View {
             .padding(.bottom, dynamicBottomPadding)
         }
         .onChange(of: viewModel.text) { newValue in
-            if newValue.count > WordsCountViewModel.maxCharacters {
-                viewModel.text = String(newValue.prefix(WordsCountViewModel.maxCharacters))
+            if newValue.wordCount > WordsCountViewModel.maxWords {
+                viewModel.text = newValue.truncated(toMaxWords: WordsCountViewModel.maxWords)
             }
         }
     }
@@ -78,7 +78,7 @@ struct WordsCountView: View {
 
             statsRow
 
-            ZStack(alignment: .bottomTrailing) {
+            VStack(alignment: .leading, spacing: 6) {
                 ThinCaretTextEditor(
                     text: $viewModel.text,
                     placeholder: "Type or paste your text here to count words..."
@@ -94,11 +94,10 @@ struct WordsCountView: View {
                             .stroke(AppTheme.inputBorder, lineWidth: 1)
                     )
 
-                Text(viewModel.characterCountLabel)
+                Text(viewModel.wordCountLimitLabel)
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
-                    .padding(.trailing, 16)
-                    .padding(.bottom, 12)
+                    .frame(maxWidth: .infinity, alignment: .trailing)
             }
             .frame(maxHeight: .infinity)