style: correct translation header and footer layout
This commit is contained in:
@@ -77,7 +77,7 @@ Currency and crypto conversions require internet access and use [currency-api](h
|
|||||||
|
|
||||||
## Translation
|
## 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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import ConjureKit
|
|||||||
@MainActor
|
@MainActor
|
||||||
final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
|
final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
|
||||||
private let searchField = NSSearchField()
|
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 tableView = ApplicationTableView()
|
||||||
private let statusLabel = NSTextField(labelWithString: "Finding applications…")
|
private let statusLabel = NSTextField(labelWithString: "Finding applications…")
|
||||||
private let emptyLabel = 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.delegate = self
|
||||||
searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies")
|
searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies")
|
||||||
|
|
||||||
let symbol = IconView(font: searchFont)
|
searchSymbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
|
||||||
symbol.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
|
searchSymbol.contentTintColor = SunsetPalette.accent
|
||||||
symbol.contentTintColor = SunsetPalette.accent
|
translationBack.image = IconStyle.symbol("chevron.left", for: searchFont)
|
||||||
let header = NSStackView(views: [symbol, searchField])
|
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.orientation = .horizontal
|
||||||
header.alignment = .centerY
|
header.alignment = .centerY
|
||||||
header.spacing = 12
|
header.spacing = 12
|
||||||
@@ -780,6 +792,9 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
guard let self, let controller else { return }
|
guard let self, let controller else { return }
|
||||||
controller.view.removeFromSuperview()
|
controller.view.removeFromSuperview()
|
||||||
self.translation = nil
|
self.translation = nil
|
||||||
|
self.translationBack.isHidden = true
|
||||||
|
self.translationBack.target = nil
|
||||||
|
self.searchSymbol.isHidden = false
|
||||||
for view in self.launcherViews.dropFirst() {
|
for view in self.launcherViews.dropFirst() {
|
||||||
view.isHidden = false
|
view.isHidden = false
|
||||||
view.setAccessibilityHidden(false)
|
view.setAccessibilityHidden(false)
|
||||||
@@ -795,6 +810,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
actions.close()
|
actions.close()
|
||||||
(window as? LauncherPanel)?.resetNumberHints()
|
(window as? LauncherPanel)?.resetNumberHints()
|
||||||
openButton.keyEquivalent = ""
|
openButton.keyEquivalent = ""
|
||||||
|
searchSymbol.isHidden = true
|
||||||
|
translationBack.target = controller
|
||||||
|
translationBack.action = #selector(TranslationController.goBack)
|
||||||
|
translationBack.isHidden = false
|
||||||
searchField.stringValue = ""
|
searchField.stringValue = ""
|
||||||
searchField.placeholderAttributedString = NSAttributedString(
|
searchField.placeholderAttributedString = NSAttributedString(
|
||||||
string: "Enter English text to translate…",
|
string: "Enter English text to translate…",
|
||||||
|
|||||||
@@ -26,16 +26,6 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText
|
|||||||
let title = NSTextField(labelWithString: "Conjure Translate · English → Japanese")
|
let title = NSTextField(labelWithString: "Conjure Translate · English → Japanese")
|
||||||
title.font = .systemFont(ofSize: 13, weight: .semibold)
|
title.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||||
title.textColor = SunsetPalette.text
|
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.addItems(withTitles: ["MyMemory (no key)", "OpenAI · gpt-6-luna"])
|
||||||
provider.selectItem(at: UserDefaults.standard.string(forKey: "translationProvider") == "openai" ? 1 : 0)
|
provider.selectItem(at: UserDefaults.standard.string(forKey: "translationProvider") == "openai" ? 1 : 0)
|
||||||
provider.setAccessibilityLabel("Translation provider")
|
provider.setAccessibilityLabel("Translation provider")
|
||||||
@@ -75,8 +65,10 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText
|
|||||||
returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
|
returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
|
||||||
returnKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12),
|
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.orientation = .horizontal
|
||||||
|
footer.alignment = .centerY
|
||||||
|
footer.spacing = 12
|
||||||
footer.translatesAutoresizingMaskIntoConstraints = false
|
footer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(footer)
|
view.addSubview(footer)
|
||||||
output.isEditable = false
|
output.isEditable = false
|
||||||
@@ -98,7 +90,8 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText
|
|||||||
disclosure.setAccessibilityLabel("Translation provider, privacy, and limits")
|
disclosure.setAccessibilityLabel("Translation provider, privacy, and limits")
|
||||||
status.font = .systemFont(ofSize: 12)
|
status.font = .systemFont(ofSize: 12)
|
||||||
status.setAccessibilityLabel("Translation status and errors")
|
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.orientation = .vertical
|
||||||
stack.alignment = .leading
|
stack.alignment = .leading
|
||||||
stack.spacing = 12
|
stack.spacing = 12
|
||||||
@@ -106,8 +99,6 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText
|
|||||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(stack)
|
view.addSubview(stack)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
back.widthAnchor.constraint(equalToConstant: 22),
|
|
||||||
back.heightAnchor.constraint(equalToConstant: 22),
|
|
||||||
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
||||||
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
|
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
|
||||||
stack.topAnchor.constraint(equalTo: view.topAnchor),
|
stack.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
@@ -120,7 +111,6 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText
|
|||||||
disclosure.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
disclosure.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
scroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
scroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 100),
|
scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 100),
|
||||||
status.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
|
||||||
])
|
])
|
||||||
updateProvider()
|
updateProvider()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,3 @@
|
|||||||
- [ ] improve on the conjure translation command
|
- [ ] add an emoji picker inspired raycast's emoji picker
|
||||||
|
- [ ] add a notes app inspired by raycast notes
|
||||||
## UI consistency audit
|
- [ ] improve on the conjure translation command
|
||||||
|
|
||||||
### 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.
|
|
||||||
@@ -47,4 +47,14 @@ import Testing
|
|||||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
|
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
|
||||||
#expect(search.stringValue == "translate")
|
#expect(search.stringValue == "translate")
|
||||||
#expect(table.numberOfRows == 1)
|
#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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user