From 6574c97276aac8f3b44833271b573698d317b79c Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Thu, 15 Oct 2026 13:43:40 +0200 Subject: [PATCH] style: correct translation header and footer layout --- README.md | 2 +- .../Conjure/LauncherWindowController.swift | 27 +++++++++++++--- Sources/Conjure/TranslationController.swift | 20 +++--------- TODO.md | 31 ++----------------- Tests/ConjureKitTests/TranslationTests.swift | 10 ++++++ 5 files changed, 42 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index e28230d..dbec387 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Currency and crypto conversions require internet access and use [currency-api](h ## Translation -Search for **Conjure Translate** and open it. Enter English in the search bar, choose a provider, then press Return or click **Translate**. Typing alone never sends text. Copy selected Japanese output with Command-C; Back or Escape returns to the launcher. +Search for **Conjure Translate** and open it. Enter English in the search bar, choose a provider, then press Return or click **Translate**. Typing alone never sends text. Copy selected Japanese output with Command-C; the copy hint appears beside the Translate action at the bottom. The Back chevron beside the input or Escape returns to the launcher and restores your search. - **MyMemory:** No account or key required. Limits are 500 UTF-8 bytes per request and [5,000 characters per day](https://mymemory.translated.net/doc/usagelimits.php) for anonymous use. - **OpenAI:** Requires an API key, access to `gpt-6-luna`, and sufficient API billing. Choose **Save Key** to store it in macOS Keychain; **Forget Key** removes it. Requests disable API response storage, but OpenAI's retention policies still apply. diff --git a/Sources/Conjure/LauncherWindowController.swift b/Sources/Conjure/LauncherWindowController.swift index c172b2e..18a8500 100644 --- a/Sources/Conjure/LauncherWindowController.swift +++ b/Sources/Conjure/LauncherWindowController.swift @@ -4,6 +4,8 @@ import ConjureKit @MainActor final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate { private let searchField = NSSearchField() + private let searchSymbol = IconView(font: .systemFont(ofSize: 20)) + private let translationBack = NSButton(title: "", target: nil, action: nil) private let tableView = ApplicationTableView() private let statusLabel = NSTextField(labelWithString: "Finding applications…") private let emptyLabel = NSTextField(labelWithString: "Finding applications…") @@ -226,10 +228,20 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe searchField.delegate = self searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies") - let symbol = IconView(font: searchFont) - symbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil) - symbol.contentTintColor = SunsetPalette.accent - let header = NSStackView(views: [symbol, searchField]) + searchSymbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil) + searchSymbol.contentTintColor = SunsetPalette.accent + translationBack.image = IconStyle.symbol("chevron.left", for: searchFont) + translationBack.imageScaling = .scaleProportionallyDown + translationBack.isBordered = false + translationBack.contentTintColor = SunsetPalette.secondaryText + translationBack.setAccessibilityLabel("Back to applications") + translationBack.toolTip = "Back to applications (Escape)" + translationBack.isHidden = true + NSLayoutConstraint.activate([ + translationBack.widthAnchor.constraint(equalToConstant: IconStyle.applicationSize(for: searchFont)), + translationBack.heightAnchor.constraint(equalToConstant: IconStyle.applicationSize(for: searchFont)), + ]) + let header = NSStackView(views: [searchSymbol, translationBack, searchField]) header.orientation = .horizontal header.alignment = .centerY header.spacing = 12 @@ -780,6 +792,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe guard let self, let controller else { return } controller.view.removeFromSuperview() self.translation = nil + self.translationBack.isHidden = true + self.translationBack.target = nil + self.searchSymbol.isHidden = false for view in self.launcherViews.dropFirst() { view.isHidden = false view.setAccessibilityHidden(false) @@ -795,6 +810,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe actions.close() (window as? LauncherPanel)?.resetNumberHints() openButton.keyEquivalent = "" + searchSymbol.isHidden = true + translationBack.target = controller + translationBack.action = #selector(TranslationController.goBack) + translationBack.isHidden = false searchField.stringValue = "" searchField.placeholderAttributedString = NSAttributedString( string: "Enter English text to translate…", diff --git a/Sources/Conjure/TranslationController.swift b/Sources/Conjure/TranslationController.swift index 4ec8317..9cfddab 100644 --- a/Sources/Conjure/TranslationController.swift +++ b/Sources/Conjure/TranslationController.swift @@ -26,16 +26,6 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText let title = NSTextField(labelWithString: "Conjure Translate · English → Japanese") title.font = .systemFont(ofSize: 13, weight: .semibold) title.textColor = SunsetPalette.text - let back = NSButton(title: "", target: self, action: #selector(goBack)) - back.image = IconStyle.symbol("chevron.left", for: title.font!) - back.imageScaling = .scaleProportionallyDown - back.isBordered = false - back.contentTintColor = SunsetPalette.secondaryText - back.setAccessibilityLabel("Back to applications") - back.toolTip = "Back to applications (Escape)" - let heading = NSStackView(views: [back, title, NSView()]) - heading.alignment = .centerY - heading.spacing = 12 provider.addItems(withTitles: ["MyMemory (no key)", "OpenAI · gpt-6-luna"]) provider.selectItem(at: UserDefaults.standard.string(forKey: "translationProvider") == "openai" ? 1 : 0) provider.setAccessibilityLabel("Translation provider") @@ -75,8 +65,10 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor), returnKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12), ]) - let footer = NSStackView(views: [NSView(), actionPill]) + let footer = NSStackView(views: [status, NSView(), actionPill]) footer.orientation = .horizontal + footer.alignment = .centerY + footer.spacing = 12 footer.translatesAutoresizingMaskIntoConstraints = false view.addSubview(footer) output.isEditable = false @@ -98,7 +90,8 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText disclosure.setAccessibilityLabel("Translation provider, privacy, and limits") status.font = .systemFont(ofSize: 12) status.setAccessibilityLabel("Translation status and errors") - let stack = NSStackView(views: [heading, provider, disclosure, credentials, scroll, status]) + status.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + let stack = NSStackView(views: [title, provider, disclosure, credentials, scroll]) stack.orientation = .vertical stack.alignment = .leading stack.spacing = 12 @@ -106,8 +99,6 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) NSLayoutConstraint.activate([ - back.widthAnchor.constraint(equalToConstant: 22), - back.heightAnchor.constraint(equalToConstant: 22), stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), stack.topAnchor.constraint(equalTo: view.topAnchor), @@ -120,7 +111,6 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText disclosure.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 100), - status.widthAnchor.constraint(equalTo: stack.widthAnchor), ]) updateProvider() } diff --git a/TODO.md b/TODO.md index de6d24c..72f124d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,28 +1,3 @@ -- [ ] improve on the conjure translation command - -## UI consistency audit - -### Navigation, context, and feedback - -- [x] Add a visible, accessible Back action to Translate, matching uninstall review; retain Escape and query/focus restoration. -- [x] Keep the selected application's name visible throughout uninstall file review, not only in Actions and final confirmation. -- [x] Preserve uninstall discovery loading/error feedback when filtering; show selection counts only after successful discovery. -- [x] Standardize feedback severity: secondary text for guidance/progress and consistent error styling for actual failures. Shortcut modifier guidance should not appear red; login and translation failures should receive error treatment. -- [x] Show persistent uninstall keyboard guidance: Return toggles file inclusion; Command-Return opens confirmation. Preserve the safer shortcut distinction. -- [x] Separate Translate's persistent provider/privacy/limit disclosure from transient validation, credential, loading, success, and error feedback. - -### Visual consistency - -- [x] Match uninstall footer text to the launcher/Translate baseline: 12pt medium rather than 14pt; retain destructive red. -- [x] Align the uninstall footer with launcher/Translate: 8pt right/bottom insets rather than 24pt right and 16pt bottom. -- [x] Use the launcher/Translate footer's plain brown-filled view for uninstall instead of adding a popover material and orange tint. -- [x] Apply Actions' SF Symbol optical-inset handling to launcher icons. Native layout measured a 22×29pt command icon view versus 22×22pt action icon views. -- [x] Render application and command favorite markers consistently: use the configured star symbol rather than a literal ★ for commands. -- [x] Apply the existing Sunset palette to Translate's custom heading and the Settings recovery button, matching equivalent headings/actions. - -### Visual verification - -- [ ] Visually check footer weight, symbol alignment, heading colors, and backdrop-dependent contrast in the live app; offscreen native checks do not establish final appearance. -- [ ] Check expanded Settings states, including multiline errors, login approval, and uninstall recovery. The fixed, non-scrolling window may compress or clip content; this remains unverified. - -Keep intentional differences: taller two-line uninstall rows, larger translation output, native Settings controls, and destructive confirmation shortcuts. \ No newline at end of file +- [ ] add an emoji picker inspired raycast's emoji picker +- [ ] add a notes app inspired by raycast notes +- [ ] improve on the conjure translation command \ No newline at end of file diff --git a/Tests/ConjureKitTests/TranslationTests.swift b/Tests/ConjureKitTests/TranslationTests.swift index fbd3f37..f3a4007 100644 --- a/Tests/ConjureKitTests/TranslationTests.swift +++ b/Tests/ConjureKitTests/TranslationTests.swift @@ -47,4 +47,14 @@ import Testing _ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:))) #expect(search.stringValue == "translate") #expect(table.numberOfRows == 1) + _ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:))) + let back = try #require(descendants(content).compactMap { $0 as? NSButton }.first { + !$0.isHidden && $0.accessibilityLabel() == "Back to applications" + }) + search.stringValue = "Hello" + launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search)) + back.performClick(nil) + #expect(search.stringValue == "translate") + #expect(back.isHidden) + #expect(table.numberOfRows == 1) }